APIv4 Search: Improve GROUP_CONCAT with :label prefix
authorColeman Watts <coleman@civicrm.org>
Wed, 23 Sep 2020 18:35:40 +0000 (14:35 -0400)
committerColeman Watts <coleman@civicrm.org>
Thu, 24 Sep 2020 12:14:12 +0000 (08:14 -0400)
This moves handing of pseudoconstant suffix replacement from the UI level to the API level,
which allows APIv4 to return GROUP_CONCAT results as an array (by default, if no separator specified).

It also improves post-query formatting in general, with finer-grained formating callbacks for sql functions.

16 files changed:
Civi/Api4/Query/Api4SelectQuery.php
Civi/Api4/Query/SqlExpression.php
Civi/Api4/Query/SqlField.php
Civi/Api4/Query/SqlFunction.php
Civi/Api4/Query/SqlFunctionCOUNT.php
Civi/Api4/Query/SqlFunctionGREATEST.php
Civi/Api4/Query/SqlFunctionGROUP_CONCAT.php
Civi/Api4/Query/SqlFunctionLEAST.php
Civi/Api4/Query/SqlFunctionMAX.php
Civi/Api4/Query/SqlFunctionMIN.php
Civi/Api4/Query/SqlFunctionNULLIF.php
Civi/Api4/Utils/FormattingUtil.php
ext/search/ang/search/crmSearch.component.js
ext/search/ang/search/crmSearchFunction.component.js
tests/phpunit/api/v4/Action/SqlFunctionTest.php
tests/phpunit/api/v4/Query/SqlExpressionParserTest.php

index 009095ebc91d3e150027b5c5e19a20041e98c75b..a1654769af00edc965a77e5263f174ddccf67a1c 100644 (file)
@@ -138,7 +138,7 @@ class Api4SelectQuery {
       }
       $results[] = $result;
     }
-    FormattingUtil::formatOutputValues($results, $this->apiFieldSpec, $this->getEntity());
+    FormattingUtil::formatOutputValues($results, $this->apiFieldSpec, $this->getEntity(), 'get', $this->selectAliases);
     return $results;
   }
 
index 2759d3baff0a72b28302685f7f119815dc81cc79..bce3d47144da3938a007f248ec7e7267891f07bc 100644 (file)
@@ -37,6 +37,14 @@ abstract class SqlExpression {
    */
   public $expr = '';
 
+  /**
+   * Whether or not pseudoconstant suffixes should be evaluated during output.
+   *
+   * @var bool
+   * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
+   */
+  public $supportsExpansion = FALSE;
+
   /**
    * SqlFunction constructor.
    * @param string $expr
index 67f1c206a6ff73dc2ce09aebe158bb49c265c6b8..b6f69f4d5fb0744df03265abd522fa3b9c1796b5 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlField extends SqlExpression {
 
+  public $supportsExpansion = TRUE;
+
   protected function initialize() {
     if ($this->alias && $this->alias !== $this->expr) {
       throw new \API_Exception("Aliasing field names is not allowed, only expressions can have an alias.");
index dbc245f7bfc40dfd5e3ed0c5e6dbc74a47614b75..ef5d7747f259a5b5252f137a4e1dd08a1d808e3a 100644 (file)
@@ -18,8 +18,14 @@ namespace Civi\Api4\Query;
  */
 abstract class SqlFunction extends SqlExpression {
 
+  /**
+   * @var array
+   */
   protected static $params = [];
 
+  /**
+   * @var array[]
+   */
   protected $args = [];
 
   /**
@@ -40,17 +46,22 @@ abstract class SqlFunction extends SqlExpression {
    */
   protected function initialize() {
     $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
-    foreach ($this->getParams() as $param) {
+    foreach ($this->getParams() as $idx => $param) {
       $prefix = $this->captureKeyword($param['prefix'], $arg);
+      $this->args[$idx] = [
+        'prefix' => $prefix,
+        'expr' => [],
+        'suffix' => NULL,
+      ];
       if ($param['expr'] && isset($prefix) || in_array('', $param['prefix']) || !$param['optional']) {
-        $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']);
-        $this->captureKeyword($param['suffix'], $arg);
+        $this->args[$idx]['expr'] = $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']);
+        $this->args[$idx]['suffix'] = $this->captureKeyword($param['suffix'], $arg);
       }
     }
   }
 
   /**
-   * Shift a keyword off the beginning of the argument string and into the argument array.
+   * Shift a keyword off the beginning of the argument string and return it.
    *
    * @param array $keywords
    *   Whitelist of keywords
@@ -60,7 +71,6 @@ abstract class SqlFunction extends SqlExpression {
   private function captureKeyword($keywords, &$arg) {
     foreach (array_filter($keywords) as $key) {
       if (strpos($arg, $key . ' ') === 0) {
-        $this->args[] = $key;
         $arg = ltrim(substr($arg, strlen($key)));
         return $key;
       }
@@ -69,35 +79,34 @@ abstract class SqlFunction extends SqlExpression {
   }
 
   /**
-   * Shifts 0 or more expressions off the argument string and into the argument array
+   * Shifts 0 or more expressions off the argument string and returns them
    *
    * @param string $arg
    * @param int $limit
    * @param array $mustBe
    * @param array $cantBe
+   * @return array
    * @throws \API_Exception
    */
   private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) {
-    $captured = 0;
+    $captured = [];
     $arg = ltrim($arg);
     while ($arg) {
       $item = $this->captureExpression($arg);
       $arg = ltrim(substr($arg, strlen($item)));
       $expr = SqlExpression::convert($item, FALSE, $mustBe, $cantBe);
       $this->fields = array_merge($this->fields, $expr->getFields());
-      if ($captured) {
-        $this->args[] = ',';
-      }
-      $this->args[] = $expr;
+      $captured[] = $expr;
       $captured++;
       // Keep going if we have a comma indicating another expression follows
-      if ($captured < $limit && substr($arg, 0, 1) === ',') {
+      if (count($captured) < $limit && substr($arg, 0, 1) === ',') {
         $arg = ltrim(substr($arg, 1));
       }
       else {
-        return;
+        break;
       }
     }
+    return $captured;
   }
 
   /**
@@ -147,20 +156,50 @@ abstract class SqlFunction extends SqlExpression {
     return $item;
   }
 
+  /**
+   * Render the expression for insertion into the sql query
+   *
+   * @param array $fieldList
+   * @return string
+   */
   public function render(array $fieldList): string {
-    $output = $this->getName() . '(';
+    $output = '';
+    $params = $this->getParams();
     foreach ($this->args as $index => $arg) {
-      if ($index && $arg !== ',') {
-        $output .= ' ';
-      }
-      if (is_object($arg)) {
-        $output .= $arg->render($fieldList);
+      $rendered = $this->renderArg($arg, $params[$index], $fieldList);
+      if (strlen($rendered)) {
+        $output .= (strlen($output) ? ' ' : '') . $rendered;
       }
-      else {
-        $output .= $arg;
+    }
+    return $this->getName() . '(' . $output . ')';
+  }
+
+  /**
+   * @param array $arg
+   * @param array $param
+   * @param array $fieldList
+   * @return string
+   */
+  private function renderArg($arg, $param, $fieldList): string {
+    // Supply api_default
+    if (!isset($arg['prefix']) && !isset($arg['suffix']) && empty($arg['expr']) && !empty($param['api_default'])) {
+      $arg = [
+        'prefix' => $param['api_default']['prefix'] ?? reset($param['prefix']),
+        'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr'] ?? []),
+        'suffix' => $param['api_default']['suffix'] ?? reset($param['suffix']),
+      ];
+    }
+    $rendered = $arg['prefix'] ?? '';
+    foreach ($arg['expr'] ?? [] as $idx => $expr) {
+      if (strlen($rendered) || $idx) {
+        $rendered .= $idx ? ', ' : ' ';
       }
+      $rendered .= $expr->render($fieldList);
+    }
+    if (isset($arg['suffix'])) {
+      $rendered .= (strlen($rendered) ? ' ' : '') . $arg['suffix'];
     }
-    return $output . ')';
+    return $rendered;
   }
 
   /**
@@ -194,11 +233,20 @@ abstract class SqlFunction extends SqlExpression {
         'optional' => FALSE,
         'must_be' => [],
         'cant_be' => ['SqlWild'],
+        'api_default' => NULL,
       ];
     }
     return $params;
   }
 
+  /**
+   * Get the arguments passed to this sql function instance.
+   * @return array[]
+   */
+  public function getArgs(): array {
+    return $this->args;
+  }
+
   /**
    * @return string
    */
index f149108a8d9e665902937b93209e5507155942a2..2ab3d661630f9da57e8944502ddfb8f70e854c60 100644 (file)
@@ -27,6 +27,17 @@ class SqlFunctionCOUNT extends SqlFunction {
     ],
   ];
 
+  /**
+   * Reformat result as array if using default separator
+   *
+   * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
+   * @param string $value
+   * @return string|array
+   */
+  public function formatOutputValue($value) {
+    return (int) $value;
+  }
+
   /**
    * @return string
    */
index 755ab2815112005b743315f702436f6dd1203ed8..5ce1b496a78ea00aae4b7140c5a5716b9695f776 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionGREATEST extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_COMPARISON;
 
   protected static $params = [
index 869bd077bfea47280a749f8535af1b51c2756c32..b683f15a72362e68dc250a296a6f5200031afcb4 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionGROUP_CONCAT extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_AGGREGATE;
 
   protected static $params = [
@@ -37,9 +39,28 @@ class SqlFunctionGROUP_CONCAT extends SqlFunction {
       'expr' => 1,
       'must_be' => ['SqlString'],
       'optional' => TRUE,
+      // @see self::formatOutput()
+      'api_default' => [
+        'expr' => ['"' . \CRM_Core_DAO::VALUE_SEPARATOR . '"'],
+      ],
     ],
   ];
 
+  /**
+   * Reformat result as array if using default separator
+   *
+   * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
+   * @param string $value
+   * @return string|array
+   */
+  public function formatOutputValue($value) {
+    $exprArgs = $this->getArgs();
+    if (!$exprArgs[2]['prefix']) {
+      $value = explode(\CRM_Core_DAO::VALUE_SEPARATOR, $value);
+    }
+    return $value;
+  }
+
   /**
    * @return string
    */
index ea246a820da7e63803bffdff3e794da006fc973d..b0f315a98822c8423be15c632cdf8d5b62450040 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionLEAST extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_COMPARISON;
 
   protected static $params = [
index 2116ec19f3744691019088c0e94e0a189376104e..c783d2cab712332f205ef60704364a691c135678 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionMAX extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_AGGREGATE;
 
   protected static $params = [
index e8d4c56ebb3a13e6cc8246542ff0cc2632a171ae..f5fe4e86bd5793158142d0817b6796da128ce8da 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionMIN extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_AGGREGATE;
 
   protected static $params = [
index 846981c736b1f9508036f1947273161cc72d1731..53ec601bcce27e032b6888fe2f40ffaa08dfc067 100644 (file)
@@ -16,6 +16,8 @@ namespace Civi\Api4\Query;
  */
 class SqlFunctionNULLIF extends SqlFunction {
 
+  public $supportsExpansion = TRUE;
+
   protected static $category = self::CATEGORY_COMPARISON;
 
   protected static $params = [
index f170969a7fda0bd58219f04d50f39038e9e8c827..82e1f770059e2d468cf367d56b1ce53afcc2712f 100644 (file)
@@ -19,6 +19,8 @@
 
 namespace Civi\Api4\Utils;
 
+use Civi\Api4\Query\SqlExpression;
+
 require_once 'api/v3/utils.php';
 
 class FormattingUtil {
@@ -134,38 +136,47 @@ class FormattingUtil {
    * @param array $fields
    * @param string $entity
    * @param string $action
+   * @param array $selectAliases
    * @throws \API_Exception
    * @throws \CRM_Core_Exception
    */
-  public static function formatOutputValues(&$results, $fields, $entity, $action = 'get') {
+  public static function formatOutputValues(&$results, $fields, $entity, $action = 'get', $selectAliases = []) {
     $fieldOptions = [];
     foreach ($results as &$result) {
       $contactTypePaths = [];
-      foreach ($result as $fieldExpr => $value) {
-        $field = $fields[$fieldExpr] ?? NULL;
-        $dataType = $field['data_type'] ?? ($fieldExpr == 'id' ? 'Integer' : NULL);
-        if ($field) {
-          // Evaluate pseudoconstant suffixes
-          $suffix = strrpos($fieldExpr, ':');
-          if ($suffix) {
-            $fieldOptions[$fieldExpr] = $fieldOptions[$fieldExpr] ?? self::getPseudoconstantList($field, substr($fieldExpr, $suffix + 1), $result, $action);
-            $dataType = NULL;
-          }
-          if (!empty($field['serialize'])) {
-            if (is_string($value)) {
-              $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']);
-            }
-          }
-          if (isset($fieldOptions[$fieldExpr])) {
-            $value = self::replacePseudoconstant($fieldOptions[$fieldExpr], $value);
+      foreach ($result as $key => $value) {
+        $fieldExpr = SqlExpression::convert($selectAliases[$key] ?? $key);
+        $fieldName = \CRM_Utils_Array::first($fieldExpr->getFields());
+        $field = $fieldName && isset($fields[$fieldName]) ? $fields[$fieldName] : NULL;
+        $dataType = $field['data_type'] ?? ($fieldName == 'id' ? 'Integer' : NULL);
+        // If Sql Function e.g. GROUP_CONCAT or COUNT wants to do its own formatting, apply and skip dataType conversion
+        if (method_exists($fieldExpr, 'formatOutputValue') && is_string($value)) {
+          $result[$key] = $value = $fieldExpr->formatOutputValue($value);
+          $dataType = NULL;
+        }
+        if (!$field) {
+          continue;
+        }
+        // Evaluate pseudoconstant suffixes
+        $suffix = strrpos($fieldName, ':');
+        if ($suffix) {
+          $fieldOptions[$fieldName] = $fieldOptions[$fieldName] ?? self::getPseudoconstantList($field, substr($fieldName, $suffix + 1), $result, $action);
+          $dataType = NULL;
+        }
+        if ($fieldExpr->supportsExpansion) {
+          if (!empty($field['serialize']) && is_string($value)) {
+            $value = \CRM_Core_DAO::unSerializeField($value, $field['serialize']);
           }
-          // Keep track of contact types for self::contactFieldsToRemove
-          if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
-            $prefix = strrpos($fieldExpr, '.');
-            $contactTypePaths[$prefix ? substr($fieldExpr, 0, $prefix + 1) : ''] = $value;
+          if (isset($fieldOptions[$fieldName])) {
+            $value = self::replacePseudoconstant($fieldOptions[$fieldName], $value);
           }
         }
-        $result[$fieldExpr] = self::convertDataType($value, $dataType);
+        // Keep track of contact types for self::contactFieldsToRemove
+        if ($value && isset($field['entity']) && $field['entity'] === 'Contact' && $field['name'] === 'contact_type') {
+          $prefix = strrpos($fieldName, '.');
+          $contactTypePaths[$prefix ? substr($fieldName, 0, $prefix + 1) : ''] = $value;
+        }
+        $result[$key] = self::convertDataType($value, $dataType);
       }
       // Remove inapplicable contact fields
       foreach ($contactTypePaths as $prefix => $contactType) {
index 9c7b7854c62113829cf7589dce784834ce95e4b3..88ba4c7b11d4090d9d8b6d4a79f4edd255a7e357 100644 (file)
         var info = searchMeta.parseExpr(col),
           key = info.fn ? (info.fn.name + ':' + info.path + info.suffix) : col,
           value = row[key];
-        // Handle grouped results
-        if (info.fn && info.fn.name === 'GROUP_CONCAT' && value) {
-          return formatGroupConcatValues(info, value);
-        }
-        else if (info.fn && info.fn.name === 'COUNT') {
+        if (info.fn && info.fn.name === 'COUNT') {
           return value;
         }
         return formatFieldValue(info.field, value);
 
       function formatFieldValue(field, value) {
         var type = field.data_type;
+        if (_.isArray(value)) {
+          return _.map(value, function(val) {
+            return formatFieldValue(field, val);
+          }).join(', ');
+        }
         if (value && (type === 'Date' || type === 'Timestamp') && /^\d{4}-\d{2}-\d{2}/.test(value)) {
           return CRM.utils.formatDate(value, null, type === 'Timestamp');
         }
         else if (type === 'Boolean' && typeof value === 'boolean') {
           return value ? ts('Yes') : ts('No');
         }
-        else if (type === 'Money') {
+        else if (type === 'Money' && typeof value === 'number') {
           return CRM.formatMoney(value);
         }
-        if (_.isArray(value)) {
-          return value.join(', ');
-        }
         return value;
       }
 
-      function formatGroupConcatValues(info, values) {
-        return _.transform(values.split(','), function(result, val) {
-          if (info.field.options && !info.suffix) {
-            result.push(_.result(getOption(info.field, val), 'label'));
-          } else {
-            result.push(formatFieldValue(info.field, val));
-          }
-        }).join(', ');
-      }
-
       function getOption(field, value) {
         return _.find(field.options, function(option) {
           // Type coersion is intentional
index e75bac9571480ed04dbe726c292bc093b35e6a94..7a46aff4311f18a5a566a229cfd5b932931d58f2 100644 (file)
@@ -14,7 +14,7 @@
 
       this.$onInit = function() {
         var fieldInfo = searchMeta.parseExpr(ctrl.expr);
-        ctrl.path = fieldInfo.path;
+        ctrl.path = fieldInfo.path + fieldInfo.suffix;
         ctrl.field = fieldInfo.field;
         ctrl.fn = !fieldInfo.fn ? '' : fieldInfo.fn.name;
       };
index af91acffaff4d63c8d839258ed17369374eaa74c..0ff7f2cec13a2f4cef5c26ca77814a25a0266b70 100644 (file)
@@ -39,14 +39,16 @@ class SqlFunctionTest extends UnitTestCase {
   public function testGroupAggregates() {
     $cid = Contact::create(FALSE)->addValue('first_name', 'bill')->execute()->first()['id'];
     Contribution::save(FALSE)
-      ->setDefaults(['contact_id' => $cid, 'financial_type_id' => 1])
+      ->setDefaults(['contact_id' => $cid, 'financial_type_id:name' => 'Donation'])
       ->setRecords([
         ['total_amount' => 100, 'receive_date' => '2020-01-01'],
         ['total_amount' => 200, 'receive_date' => '2020-01-01'],
-        ['total_amount' => 300, 'receive_date' => '2020-01-01'],
-        ['total_amount' => 400, 'receive_date' => '2020-01-01'],
+        ['total_amount' => 300, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Member Dues'],
+        ['total_amount' => 400, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Event Fee'],
       ])
       ->execute();
+
+    // Test AVG, SUM, MAX, MIN, COUNT
     $agg = Contribution::get(FALSE)
       ->addGroupBy('contact_id')
       ->addWhere('contact_id', '=', $cid)
@@ -57,11 +59,23 @@ class SqlFunctionTest extends UnitTestCase {
       ->addSelect('COUNT(*) AS count')
       ->execute()
       ->first();
-    $this->assertEquals(250, $agg['average']);
-    $this->assertEquals(1000, $agg['SUM:total_amount']);
-    $this->assertEquals(400, $agg['MAX:total_amount']);
-    $this->assertEquals(100, $agg['MIN:total_amount']);
-    $this->assertEquals(4, $agg['count']);
+    $this->assertTrue(250.0 === $agg['average']);
+    $this->assertTrue(1000.0 === $agg['SUM:total_amount']);
+    $this->assertTrue(400.0 === $agg['MAX:total_amount']);
+    $this->assertTrue(100.0 === $agg['MIN:total_amount']);
+    $this->assertTrue(4 === $agg['count']);
+
+    // Test GROUP_CONCAT
+    $agg = Contribution::get(FALSE)
+      ->addGroupBy('contact_id')
+      ->addWhere('contact_id', '=', $cid)
+      ->addSelect('GROUP_CONCAT(financial_type_id:name)')
+      ->addSelect('COUNT(*) AS count')
+      ->execute()
+      ->first();
+
+    $this->assertTrue(4 === $agg['count']);
+    $this->assertContains('Donation', $agg['GROUP_CONCAT:financial_type_id:name']);
   }
 
   public function testGroupHaving() {
index 2f18648837f321c4471d125eeaf5a29485037830..9532529250a57d381ff1a391ef3a9d5949129609 100644 (file)
@@ -50,19 +50,31 @@ class SqlExpressionParserTest extends UnitTestCase {
     $sqlFn = new $className($fnName . '(total)');
     $this->assertEquals($fnName, $sqlFn->getName());
     $this->assertEquals(['total'], $sqlFn->getFields());
-    $this->assertCount(1, $this->getArgs($sqlFn));
+    $args = $sqlFn->getArgs();
+    $this->assertCount(1, $args);
+    $this->assertNull($args[0]['prefix']);
+    $this->assertNull($args[0]['suffix']);
+    $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlField'));
 
     $sqlFn = SqlExpression::convert($fnName . '(DISTINCT stuff)');
     $this->assertEquals($fnName, $sqlFn->getName());
     $this->assertEquals("Civi\Api4\Query\SqlFunction$fnName", get_class($sqlFn));
     $this->assertEquals($params, $sqlFn->getParams());
     $this->assertEquals(['stuff'], $sqlFn->getFields());
-    $this->assertCount(2, $this->getArgs($sqlFn));
+    $args = $sqlFn->getArgs();
+    $this->assertCount(1, $args);
+    $this->assertEquals('DISTINCT', $args[0]['prefix']);
+    $this->assertNull($args[0]['suffix']);
+    $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlField'));
 
     try {
       $sqlFn = SqlExpression::convert($fnName . '(*)');
       if ($fnName === 'COUNT') {
-        $this->assertTrue(is_a($this->getArgs($sqlFn)[0], 'Civi\Api4\Query\SqlWild'));
+        $args = $sqlFn->getArgs();
+        $this->assertCount(1, $args);
+        $this->assertNull($args[0]['prefix']);
+        $this->assertNull($args[0]['suffix']);
+        $this->assertTrue(is_a($args[0]['expr'][0], 'Civi\Api4\Query\SqlWild'));
       }
       else {
         $this->fail('SqlWild should only be allowed in COUNT.');
@@ -73,16 +85,4 @@ class SqlExpressionParserTest extends UnitTestCase {
     }
   }
 
-  /**
-   * @param \Civi\Api4\Query\SqlFunction $fn
-   * @return array
-   * @throws \ReflectionException
-   */
-  private function getArgs($fn) {
-    $ref = new \ReflectionClass($fn);
-    $args = $ref->getProperty('args');
-    $args->setAccessible(TRUE);
-    return $args->getValue($fn);
-  }
-
 }