Construya un árbol de matriz a partir de una matriz plana y una ID estructurada

Hold fire if you are confused by the question a second on the question because I'm not sure that it exactly makes sense.

SELECT * FROM `rules` ORDER BY `Rank_1` , `Rank_2` , `Rank_3` , `Rank_4` ASC


Copy pasta:

0   0   0   0   0
1   0   0   0   1
1   1   0   0   1.1
1   1   1   0   1.1.1 
2   0   0   0   2
4   0   0   0   4
4   1   0   0   4.1
4   1   1   0   4.1.1 
4   1   1   9
4   1   1   10 
4   1   1   11

However this data is not quite in a form I need it to be in order to do something useful with it.

I want to loop through all the rules and depending on how 'deep' I am, do different things. For example I'd like to take RuleID = 0 and do <h1>, 1.1 tienen <h2> but when it comes to 4.1.x, open up a <ul> and give each rule an <li>.

For this I figure the best way is to select the data like an array where I'd end up with:

array( 4 =>
    array( 4.1 =>
        array( 4.1.1 => 'rule content')

Then I could realise my depth and open up a <ul> tag, loop through printing out the rules at that depth etc.

What is really the best way to tackle this? I've been at it for ages and don't have a clue where to go from here to be honest. I really want the data to be in the same table. I figure I could probably solve this if they were all in different tables but I don't know that for sure it would be any easier.

I started down this route:

foreach($m as $rule) {
    $depth = count(explode('.', $rule['ruleid']));
    switch($depth) {
        case 1:
            echo '<h1>'.$rule['content'].'</h1>';
        case 2:
            echo '<h2>'.$rule['content'].'</h2>';
        case 3:
            echo '<strong>'.$rule['content'].'</strong>';
    echo '<br />\n';

Then I realised this is just going to deal with each rule entry individually, whereas my solution needs some sort of 'awareness' of where it is in the rules, so it can know when to open a tag (such as a <ul>) and then close it again when it's done echoing list items that might be present in a rule (such as "Don't do: <ul><li>this</li><li>or this</li></ul>")

Here's an example of desired output from the table of data above:

0. Introducción


4. Charla

4.1. Do's and do not's

4.1.1. Hacer:

  • Be polite
  • Sea paciente
  • Be smart

Hope some bright spark can help!

preguntado el 09 de marzo de 12 a las 15:03

maybe if you explained what you are trying to accomplish overall with this, we would better be able to help you. -

Could you add a copyable version of the input data? Writing this by hand just to get a prototype running is a bit annoying ;) -

Too few details to give any real answer. You can also sort by RuleID because '.'<'0'. -

There you go Yoshi. No I couldn't Imre, because 1 < 9 and MySQL doesn't sort naturally. -

4 Respuestas

Suppose, from your input data, you can produce an array like this:

$data = array(
  '0'        => 'content 0',
  '1'        => 'content 1',
  '1.1'      => 'content 1.1',
  '1.1.1'    => 'content 1.1.1',
  '2'        => 'content 2',
  '4'        => 'content 4',
  '4.1'      => 'content 4.1',
  '4.1.1'    => 'content 4.1.1',
  ''  => 'content',
  '' => 'content',
  '' => 'content',

which you could then transform to a tree-like structure with this:

// this will be our root node
// it has no content, but a place for children
$struct = array(
  'children' => array()

foreach ($data as $ruleID => $content) {
  //                 /\
  //                 ||
  //     for every rule id in the input data we start at
  //     the root
  //          ||
  //          \/
  $parent =& $struct;

  // we split $ruleID at the dot to get a path, which we traverse
  //            ||
  //            \/
  foreach (explode('.', $ruleID) as $val) {

    // for every entry in the path we
    // check if it's available in the current parent
    //    ||
    //    \/
    if (!isset($parent['children'][$val])) {
      // if not, we create an empty entry
      $parent['children'][$val] = array(
        'content' => '',       // no content
        'children' => array()  // no children

    //                         /\
    //                         ||
    // and then use either the new, or allready existent parent
    // as the new parent
    //      ||
    //      ||  mind the assignment by reference! this
    //      \/  is the most important part
    $parent =& $parent['children'][$val];

  // after the whole path is traversed, we can
  // finally add our content
  //                    ||
  //                    \/
  $parent['content'] = $content;



respondido 09 mar '12, 16:03

Great! I thought of doing this but I got worried I'd waste my time because I'm not good enough (which gladly your code would indicate as I've no clue as to the concept of using $struct like you have) so came here first. Thanks a lot, it's much better to be able to do this with the PHP than try to mess with the data in multiple tables etc.. Would this be the right place to explain how your code works? - escritura02392

If wanted to mix your HTML with your SQL in a way that probably breaks all sorts of style rules you could add a 'ruledepth' column and then use 'CASE' on it something like:

SELECT othercols, 
WHEN ruledepth=0 THEN CONCAT('<h1>', ruleid, '</h1>') 
WHEN ruledepth=1 THEN CONCAT('<h2>', ruleid, '</h2>') 
WHEN ruledepth=2 THEN CONCAT('<li>', rileid, '</li>')
END FROM XXX WHERE .... ORDER BY ruleid........

respondido 09 mar '12, 16:03

slight modification to OPs code.

Puede añadir $rule['ruleid'] to the echo statements for it to be exactly as your desired output.

foreach($m as $rule) {
    $depth = count(explode('.', $rule['ruleid']));
    // end previous list before starting with new rules
    if ($hasList && $depth<4) {
        echo '</ul>';
    switch($depth) {
        case 1:
            echo '<h1>'.$rule['content'].'</h1>';
        case 2:
            echo '<h2>'.$rule['content'].'</h2>';
        case 3:
            echo '<strong>'.$rule['content'].'</strong>';
        case 4:
            // start list if not already started
            if (!$hasList) {
                echo '<ul>';
            echo '<li>'.$rule['content'].'</li>';
    //echo "<br />\n";
// end last list
if ($hasList) {
    echo '</ul>';

respondido 09 mar '12, 17:03

Imre thanks for this, I'm sure it'll help someone. I did come up with this the last time I attempted to solve this problem (couple months ago) but decided for me it wouldn't be the best solution because I wanted the depth to be completely variable and even in some cases have <ul> dentro de otro <ul> etc.. Sorry I didn't specify that. - escritura02392

I'm not sure if this is exactly what you need, but maybe it will give you some ideas.

First of all, you can determine the 'depth' of your rule by doing something like this:

$depth = count(explode('.',$rule_id));

This will split your rule id on the . and make an array. Then it counts the items in the array to determine the depth.

respondido 09 mar '12, 16:03

That is, character for character, what I already did to determine the depth. But that route wasn't enough. - escritura02392

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.