List posts grouped by taxonomy and child taxonomies

I have the following post structure on my dash

<ul>
  <li>Tax 1
    <ul>
      <li>Tax 2<br>
        <span>- Post 1</span><br>
        <span>- Post 2</span>
      </li>
      <li>Tax 2
        <ul>
          <li>Tax 3<br>
            <span>- Post 1</span><br>
            <span>- Post 2</span>
          </li>
        </ul>
      </li>
    </ul>
  </li>
<li>Tax 1
    <ul>
      <li>Tax 2<br>
        <span>- Post 1</span><br>
        <span>- Post 2</span>
      </li>
      <li>Tax 2
        <ul>
          <li>Tax 3<br>
            <span>- Post 1</span><br>
            <span>- Post 2</span>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

I need to show each post as a child of the last category related to it, I created a scheme but it does not group the sub categories

    <?php 
      $member_group_terms = get_terms( 'categoria' );
    ?>

  <?php
    foreach ( $member_group_terms as $member_group_term ) {
        $member_group_query = new WP_Query( array(
            'post_type' => 'documento',
            'tax_query' => array(
                array(
                    'taxonomy' => 'categoria',
                    'field' => 'slug',
                    'terms' => array( $member_group_term->slug ),
                    'operator' => 'IN'
                )
            )
        ) );
        ?>
        <h2><?php echo $member_group_term->name; ?></h2>
        <ul>
        <?php
        if ( $member_group_query->have_posts() ) : while ( $member_group_query->have_posts() ) : $member_group_query->the_post(); ?>
            <li><?php echo the_title(); ?></li>
        <?php endwhile; endif; ?>
        </ul>
        <?php
        // Reset things, for good measure
        $member_group_query = null;
        wp_reset_postdata();
    }
  ?>

The problem here is that it does not group the subcategories, leaving them listed as if it were the level 1 category.I need it to display as I mentioned above, grouped by sub categories.

Answer

What you are looking for is a recursive function. These can be a little hard to wrap your head around sometimes, so hopefully the comments in the code work for you.

In my sample code, the cpt is staff and my taxonomy is staff-type, you’ll need to update appropriately. Also, I actively avoid setup_postdata and similar functions, but it is just for personal reasons, feel free to use them if you want.

Here’s the two functions:

/**
 * @param WP_Term[] $terms
 * @param int $parentId
 */
function render_terms(array $terms, int $parentId = 0)
{
    // Find all of the parents with this ID
    $parents = array_reduce(
        $terms,
        static function (array $carry, WP_Term $term) use ($parentId) {
            if ($term->parent === $parentId) {
                $carry[] = $term;
            }
            return $carry;
        },
        []
    );

    // If there are no parent items at this level, stop process
    if (!count($parents)) {
        return;
    }

    echo '<ul>';
    foreach ($parents as $term) {
        echo '<li>';
        //Show the term name
        echo sprintf('<strong>%1$s</strong>', esc_html($term->name));

        // Show the child terms
        render_terms($terms, $term->term_id);

        // Show the child posts
        render_posts($term);
        echo '</li>';
    }
    echo '</ul>';
}

function render_posts(WP_Term $term)
{
    $results = get_posts(
        [
            'post_type' => 'staff',
            'tax_query' => [
                [
                    'taxonomy' => 'staff-type',
                    'field' => 'term_id',
                    'terms' => $term->term_id,
                    'operator' => 'IN',
                    'include_children' => false,
                ]
            ]
        ]
    );

    if (count($results)) {
        echo '<ul>';
        foreach ($results as $result) {
            echo sprintf('<li>%1$s</li>', esc_html($result->post_title));
        }
        echo '</ul>';
    }
}

And to use them:

$all_terms = get_terms(
    [
        'taxonomy' => 'staff-type',
    ]
);

render_terms($all_terms);

You might have to play with indentation a little to match things. Also, there’s a thing called breadth-first and depth-first, and your sample appeared to be the latter where you render the hierarchy as deep as possible, then show the content. This is controlled by these two lines in render_terms:

        render_terms($terms, $term->term_id);
        render_posts($term);

If you wanted a breadth-first, you’d just swap those two.