phprecursionclosuresanonymous-functionvariable-variables

In PHP, how to recurse through a Closure when the variable name holding the anonymous function is a variable-variable


I have a list of about 100 articles, and within each article is a list of clauses. The clauses are a list of varying levels deep.

$clauses = array(
  [
    'Fields' => ['Clause' => 'clause 1', 'Status' => 'Draft']
  ],
  [
    'Fields' => ['Clause' => 'clause 2', 'Status' => 'Draft'],
    'SubClauses' => [
      [
        'Fields' => ['Clause' => 'clause 2_a', 'Status' => 'Draft'],
        'SubClauses' => [
          [
            'Fields' => ['Clause' => 'clause 2_a_1', 'Status' => 'Draft']
          ],
          [
            'Fields' => ['Clause' => 'clause 2_a_2', 'Status' => 'Draft']
          ]
        ]
      ]
    ]
  ],
  [
    'Fields' => ['Clause' => 'clause 3', 'Status' => 'Draft']
  ]
);

echo PHP_EOL;

To create an html ordered list out of the $clauses array, I built this:

function htmlList ( $clauses, $depth = 0 ) {
  
  if ( $depth == 0 ) {
    echo '<ol type="1">';
  } elseif ( $depth == 1 ) {
    echo '<ol type="i">';
  } else {
    echo '<ol type="a">';
  }
  
  foreach ( $clauses as $key => $clause ) {
    if ( isset($clauses[$key]['SubClauses']) ) {
      echo '  <li>' . $clauses[$key]['Fields']['Clause'];
      htmlList ( $clauses[$key]['SubClauses'], ++$depth );
    } elseif ( isset($clauses[$key]['Fields']) ) {
      echo '  <li>' . $clauses[$key]['Fields']['Clause'] . '</li>';
    }
  }
  
  $depth--;
  echo '  </li>';
  echo '</ol>';
}

htmlList ( $clauses );

echo PHP_EOL;

It only builds the list for a single article. When the code loops to the next article, i get a error because the function is already defined.

I want to keep the html in the template and not put it inside the code files, so I have the function in the template where it can write the html there.

I need to make the name of the function change when the next article loops, so I converted this to a closure and assigned it to a variable. I am passing the function since I need it to recurse.

$htmlList = function ( $clauses, $depth = 0 ) use ( &$htmlList ) {
  
  if ( $depth == 0 ) {
    echo '<ol type="1">';
  } elseif ( $depth == 1 ) {
    echo '<ol type="i">';
  } else {
    echo '<ol type="a">';
  }
  
  foreach ( $clauses as $key => $clause ) {
    if ( isset($clauses[$key]['SubClauses']) ) {
      echo '  <li>' . $clauses[$key]['Fields']['Clause'];
      $htmlList ( $clauses[$key]['SubClauses'], ++$depth );
    } elseif ( isset($clauses[$key]['Fields']) ) {
      echo '  <li>' . $clauses[$key]['Fields']['Clause'] . '</li>';
    }
  }
  
  $depth--;
  echo '  </li>';
  echo '</ol>';
};

$htmlList ( $clauses );

echo PHP_EOL;

This also works for a single article, but allows the name to be changed dynamically. I then made the name of the variable that holds the name of the function dynamic.

$articles = array(
    ['Title' => 'title 1', 'Status' => 'Draft'],
    ['Title' => 'title 2', 'Status' => 'Draft'],
);

for ( $i = 0; $i < sizeof($articles); $i++ ) {
  
  echo $articles[$i]['Title'] . PHP_EOL;
  
  $htmlList = 'htmlList' . '_' . $i;
  $$htmlList = function ( $clauses, $depth = 0 ) use ( &$$htmlList ) {
    
    if ( $depth == 0 ) {
      echo '<ol type="1">';
    } elseif ( $depth == 1 ) {
      echo '<ol type="i">';
    } else {
      echo '<ol type="a">';
    }
    
    foreach ( $clauses as $key => $clause ) {
      if ( isset($clauses[$key]['SubClauses']) ) {
        echo '  <li>' . $clauses[$key]['Fields']['Clause'];
        $$htmlList ( $clauses[$key]['SubClauses'], ++$depth );
      } elseif ( isset($clauses[$key]['Fields']) ) {
        echo '  <li>' . $clauses[$key]['Fields']['Clause'] . '</li>';
      }
    }
    
    $depth--;
    echo '  </li>';
    echo '</ol>';
  };
  
  $$htmlList ( $clauses );
  
}

This is where it breaks. It does not like variably named function name inside the use() and errs at the $$ because it is a variable-variable and it only allows 1 $ and I have 2 $$ since the variable name's value changes.

Would it be better to save the function in the main code outside the template so it doesn't have to build anew for each article, or would it better to put the function in the template and keep html out of the main code files? All other constructs surrounding html are generally in the template files.

How can I get the clauses to convert to an html list for each article it loops through?


Solution

  • Independently of a better code design, you can test if a function already exists before defining it with function_exists:

    if (!function_exists('htmlList')) {
        function htmlList(array $clauses): void {
            static $depth = 0;
        
            echo match($depth) {
                0       => '<ol type="1">',
                1       => '<ol type="i">',
                default => '<ol type="a">'
            };
        
            foreach($clauses as $clause) {
                if (!isset($clause['Fields']['Clause']))
                    continue;
    
                echo '<li>', $clause['Fields']['Clause'];
    
                if (isset($clause['SubClauses'])) {
                    $depth++;
                    htmlList($clause['SubClauses']);
                    $depth--;
                }
           
                echo '</li>';
           }
       
           echo '</ol>';
        }
    }