Smarty Forum Index Smarty
WARNING: All discussion is moving to https://reddit.com/r/smarty, please go there! This forum will be closing soon.

Multidimensional Array Projection (Recursive)

 
This forum is locked: you cannot post, reply to, or edit topics.   This topic is locked: you cannot edit posts or make replies.    Smarty Forum Index -> Plugins
View previous topic :: View next topic  
Author Message
HZiegler
Smarty Rookie


Joined: 04 Aug 2007
Posts: 5
Location: Germany

PostPosted: Thu Aug 16, 2007 8:38 pm    Post subject: Multidimensional Array Projection (Recursive) Reply with quote

This is a quite powerful plugin if you want to use multidimensional arrays in smarty.
I coded it for menu structures, but you can use it for a lot more.

You can save the following code into your smarty 'plugins' folder as 'function.project.php' or use the register_function method from smarty into your script.

Code:
<?php

 /**
  * Smarty [project] function plugin
  *
  * Type:     function<br>
  * Name:     projection<br>
  * Date:     16. Aug. 2007
  * Purpose:  creates a new array from a n-level array to use in smarty loops<br>
  * Input:    each parameter is explained in the smarty plugins forum<br><pre>
  *           - array     (required)
  *           - onto      (required)
  *           - first     (optional)
  *           - name      (optional)
  *           - levels    (optional)
  *           - check     (optional)</pre>
  * Examples: read smarty plugin forum
  * @author   Harald Ziegler <harald.ziegler@gmail.com>
  * @version  1.0
  */


function smarty_function_project( $params, &$smarty ) {
   if ( empty($params['array']) || !is_array($params['array']) ) {
      $smarty->trigger_error('project: parameter "array" not set or not an array');
      return;
   }

   if ( empty($params['onto']) ) {
      $smarty->trigger_error('project: parameter "onto" not set');
      return;
   }

   $array = array();
   $onto = string;
   $levels = null;
   $name = 'projection';
   $first = true;
   $check = null;// => default: is_array($value);

   foreach ( $params as $param => $value ) {
      switch ( $param ) {
          case 'array':
              $array = $value;
              break;
         case 'name':
         case 'onto':
             if ( !is_string($value) )
                $smarty->trigger_error('project: parameter "'. $param .'" is not a string', E_USER_ERROR);
             $$param = $value;
             break;
         case 'levels':
             if ( !is_numeric($value) )
               $smarty->trigger_error('project: parameter "'. $param .'" is not a number', E_USER_ERROR);
             $$param = (int) $value;
             break;
         case 'first':
             $first = (bool) $value;
             break;
         case 'check':
             $check = explode(",", str_replace(array('"', "'", " "), "", $value));
             if ( count($check) != 2 )
               $smarty->trigger_error('project: parameter "check" is not valid', E_USER_ERROR);

            if ( $smarty->security == true && !in_array($check[0], $smarty->security_settings['IF_FUNCS']) )
               $smarty->trigger_error('project: security is turned on and "'. $check[0] .'" is not in the IF_FUNCS list', E_USER_ERROR);

            if ( $check[1] !== 'value' && $check[1] !== 'key' && $check[1] !== 'both' )
               $smarty->trigger_error('project: parameter "check" is not valid. please use only "value", "key" or "both" [=> func($key, $value)]. default: check=is_array,value', E_USER_ERROR);

            if ( $check[0]{0} === '!' ) {
               $check[0] = substr($check[0], 1);
               $check[2] = true;
            }
            else
                $check[2] = false;

            if ( !is_callable($check[0], false) )
               $smarty->trigger_error('project: function "'. $check[0] .'" in "check" does not exists/is not valid', E_USER_ERROR);
             break;
         default:
            $smarty->trigger_error('project: unknown parameter "'. $param .'"');
      }
   }

   $_total = 0;
   $_deepest_level = 0;
   $_projected_array = array();
   $_i = 0;

   if ( $first )
       $_projected_array[$_i++] = array(
         'open' => true,
         'level' => 0
      );

   $_projected_array += recursive_arraywalk( $array, $smarty, $check, $levels, $_i, $_deepest_level, $_total );

   if ( $first )
       $_projected_array[$_i] = array(
         'close' => true,
         'level' => 0
      );

   $smarty->assign(array(
       $onto => $_projected_array,
      $name => array(
         'total' => $_total,
         'levels' => ($_deepest_level + 1)
         )
      )
   );
}

function recursive_arraywalk( &$array, &$smarty, &$check, $levels, &$_i, &$_deepest_level, &$_total, $level = 0) {
   $projected_array = array();
   $index = 0;
   $first = true;

   if ( $_deepest_level < $level )
      $_deepest_level = $level;

   if ( !is_array($array) )
       $smarty->trigger_error('project: parameter "array" is not an valid array.', E_USER_ERROR);

   foreach ( $array as $key => $value ) {
       if ( $check === null ) {
           $checked = is_array($value);
       }
       else {
           if ( $check[1] === 'value' )
             $checked = call_user_func($check[0], $value);
         else if ( $checke[1] === 'key' )
            $checked = call_user_func($check[0], $key);
         else
             $checked = call_user_func($check[0], $key, $value);

         if ( $check[2] )
             $checked = !$checked;
      }

      $is_array = is_array($checked);

      if ( $checked === null ) {
          // skip
      }
      else if ( $checked === true || (($is_array || $checked === false)  && (is_null($levels) || $level < $levels)) ) {
          if ( $checked === false || $is_array ) {
            $last = $_i;

            if ( $is_array ) {
                if ( count($checked) == 2 ) {
                    $val = $checked[1];
               }
               else {
                   if ( !is_array($checked[0]) ) {
                      $val = array_diff_key($value, array($checked[0] => null));
                  }
                  else {
                     $val = $checked[0];
                   }
               }
            }
            else
                $val = $value;

            $projected_array[$_i++] = array(
                'first' => $first,
               'key' => $key,
               'value' => $val,
               'level' => $level,
               'index' => $index++,
               'nr' => ++$_total,
               'last' => false
            );
             if ( $first ) {
                 $first = false;
            }
         }

         if ( $checked === false || $is_array && is_array($checked[0]) )
             continue; // only if no recursive array is available or none given by check function

         $next_level = $level + 1;

         if ( is_null($levels) || $next_level < $levels ) {
             $projected_array[$_i++] = array(
               'open' => true,
               'level' => $next_level
            );

            if ( $is_array && !is_array($checked[0]) )
                $projected_array += recursive_arraywalk( $value[$checked[0]], $smarty, $check, $levels, $_i, $_deepest_level, $_total, $next_level);
            else
               $projected_array += recursive_arraywalk( $value, $smarty, $check, $levels, $_i, $_deepest_level, $_total, $next_level);

             $projected_array[$_i++] = array(
               'close' => true,
               'level' => $next_level
            );
         }
      }
      // else do nothing
   }

   if ( isset($last) )
      $projected_array[$last]['last'] = true;

   return $projected_array;
}

?>


Note: To make this plugin work on php4 you need a array_diff_key function. You can use this function from a comment on php.net.

Required parameters:
    array [array] (The array you want to project)
    onto [string] (The variable name for the projection - the new array)


Important parameters (One possibility for errors):
    first [true, false, 0, 1] {default: true} (Open and close will be elements at the beginning and the end)
    name [string] {default: projection} (The name of the variable where information about the projection is stored. See below what exactly)


Other parameters:
    levels [integer] (How many levels you want to output. e.g. 1 means only level 0)
    check [function,param(s)] {default: is_array,value} (Your custom function to determine a new level. Read explanation below)


$projection (or set by name parameter variable):
    $projection.total (The total number of elements)
    $projection.levels (The number of levels / the deepest level. Starting from 1)


check parameter:
    The check parameter will be splittet into 2 strings.
      The first one should be a function name
        Attention: If $smarty->security is set to true, you are only allowed to use the $smarty->security_settings['IF_FUNCTION']. Read more about security in the smarty documentation.
        Note: You can negate the functions return by putting a '!' before the function

      The second one can be 'key', 'value' or 'both' (and nothing else!).
        Key is the key of your array given by the array parameter on every level
        Value is the value of your array
        e.g. is_array( $value ) / !is_numeric( $key ) / go_deeper( $key, value )

    By giving a custom function you can do quite everything.
    Your function can return 3 valiable types:
      bool: true, false
        true: Go one level down with value as array Attention: value must be an array!
        false: Store value and key as an element in current level

      null:
        Skip this current key and value and do nothing (e.g. to filter something out - see expamles below)

      array:
        the array can be:
        [ fun($value) ... $value[akey] ... ]
        array( var, array() ):
          [ ... return array(akey, $myarr) ]
          The var must be the key [akey] of one element of the given value [$value] Attention: var != array.
          The array [$myarr] can be whatever you want. You can pepare or filter everything so you only have to output it in your template (=> no php in template!)

        array( var ):
          [ ... return array( var ) ]
          The key [akey] of one element of value [$value] in the array for the next level
          The Element in the new array will be the value [$value] without the key [akey]

        array( array() ):
          [ ... return array( array() ) ]
          The element in the new array will be the returned array.
          There is no change in level and the next element will be checked.

    Please look at the examples, if you don't understand it yet

As you can imagine this checking of keys and values comes quite useful.
Especially when you have to go through the array once and filter entries out or prepare them for the template, so you or somebody else can concentrate on the output with smarty code.


You will get a new 1 dimensional array and so you can walk and start wherever you want.
Each entry of that array is an array itself. This array contains special keys, to determin where you are. like the current level(=depth) or the number of the current entry in this level.
The array can have the folloing keys:
Code:

open    [true]         = next array (entry) is one level/depth/dimension deeper
close   [true]         = next array (entry) is one level/depth/dimension higher
first   [true/false]   = the first array of this level
last    [true/false]   = the last array of this level
level   [integer]      = the current level/depth/dimension
index   [integer]      = the current entry of this level. starting from 0.
nr      [integer]      = the nr of the current entry in total
key     [mixed]         = the key of your old array
value   [mixed]         = the value of your old array or a value given by your check function


There are only 3 key-sets to differentiate:

open, level
close, level
first, last, level, index, nr, key, value

The other keys won't be set. See expamples how to use them.


Ok, now some examples [I won't put the results here, but you can simply try it by copy and past the following code]:

Arrays and check function for testing:

Code:
$navigation = array(
   'Menu 1',
   array(
      'Menu 1 - Element 1',
      'Menu 1 - Element 2',
      'Menu 1 - Element 3',
      'Menu 1 - SubMenu 1',
      array(
         'Menu 1 - SubMenu 1 - Element 1',
         'Menu 1 - SubMenu 1 - Element 2',
         'Menu 1 - SubMenu 1 - SubSubMenu 1',
         'a key' => array(
             'Menu 1 - SubMenu 1 - SubSubMenu 1 - Element 1',
             'Menu 1 - SubMenu 1 - SubSubMenu 1 - Element 2',
         ),
         'Menu 1 - SubMenu 1 - Element 3',
      ),
      'Menu 1 - Element 4',
      'Menu 1 - Element 5',
      'Menu 1 - SubMenu 2',
      array(
          'Menu 1 - SubMenu 2 - Element 1',
          'Menu 1 - SubMenu 2 - Element 2',
      ),
   ),
   'Menu 2',
   array(
      'Menu 2 - SubMenu 1',
      array(
         'Menu 2 - SubMenu 1 - Element 1',
      ),
      'Menu 2 - Element 1',
      'Menu 2 - Element 2',
      'Menu 2 - Element 3',
   ),
);

$forums = array(
   1 => array(
       'id' => 1,
       'name' => 'General',
       'parent_id' => 0,
       'sub_forums' => array(
         2 =>  array(
             'id' => 2,
             'name' => 'News and announcements',
             'parent_id' => 1,
             'sub_forums' => array(
               7 =>  array(
                  'id' => 7,
                  'name' => 'Bugs',
                  'parent_id' => 2,
                   'sub_forums' => array(
                     ),
               ),
            ),
         ),
         3 => array(
             'id' => 3,
             'name' => 'General discussion',
             'parent_id' => 1,
             'sub_forums' => array(
               ),
         ),
      )
   ),
   4 => array(
       'id' => 4,
       'name' => 'Support',
       'parent_id' => 0,
       'sub_forums' => array(
           5 => array(
               'id' => 5,
               'name' => 'Technical Support',
               'parent_id' => 4,
               'sub_forums' => array(
                   ),
         ),
         6 => array(
            'id' => 6,
            'name' => 'Support Tickets',
            'parent_id' => 4,
            'sub_forums' => array(
                ),
         )
      )
   )
);

function check_forums( &$key, &$value ) {

   if ( is_numeric($key) ) {

      if ( empty($value['sub_forums']) ) // filter ... because $_deepest_level wouldn't be correct
          return array($value);
          /*
         remove 'sub_forums':
            return array(array_diff_key($value, array('sub_forums' => null)));

         save only 'name'
             return array(
                  array('name' => $value['name'])
               );
         */
      else
         return array('sub_forums');
   }

   return null;
}

$smarty->assign('navigation', $navigation);
$smarty->assign('forums', $forums);


    Only array and onto parameters are required.
    After the project function you can do whatever you want with the 'onto' variable!
    E.g. use it in a foreach or section loop.


Code:
{project array=$navigation onto=navi}

total elements: {$projection.total}<br />
levels: {$projection.levels}<br />

{foreach from=$navi item=element name=navi}
    {if $element.open && $element.level == 1}
        <ol style='list-style-type:upper-alpha'>
    {elseif $element.close && $element.level == 1}
        </ol>
    {elseif $element.open}
        <ul>
    {elseif $element.close}
        </ul>
    {else}
       <li>
         {if $element.first}
            <i style="color:green">first: </i>
         {/if}
           {if $element.last}
            <i style="color:red">last: </i>
           {/if}

         {if $element.level == 2 && $element.index == 2}
             <b>Very Special!!: </b>
         {/if}

         {if $smarty.foreach.navi.iteration == 5}
            <b>More Special:</b>
         {/if}

         {if $element.nr == 3}
             <b><i>read this!:</i></b>
         {/if}

         {if $element.first && $element.last}
            <b>only: </b>
         {/if}

         {if $element.level == 3}
             <i>{$element.value}</i>
         {else}
            level: {$element.level} | nr: {$element.nr} | index: {$element.index} | key: {$element.key} | value: {$element.value}
         {/if}
       </li>
    {/if}
{/foreach}



    Project only ... level 0 and 1 <=> 2 levels ... of an array


Code:
{project array=$navigation onto=nav levels=2}

total elements: {$projection.total}<br />
levels: {$projection.levels}<br />

{foreach from=$nav item=element name=navi}
    {if $element.open}
        <ul>
    {elseif $element.close}
        </ul>
    {else}
      <li>{$element.value}</li>
    {/if}
{/foreach}



    Projection of an complex array, with custom check function.
    I took the array structure form a post in the sticky recursion thread


Code:
{project array=$forums onto=forum_proj check=check_forums,both}

{foreach from=$forum_proj item=forum}
    {if $forum.open}
    <ul>
    {elseif $forum.close}
    </ul>
    {else}
      <li>
      {if $forum.first}
         <i style="color:green">first:</i>
      {/if}
      {if $forum.last}
         <i style="color:red">last:</i>
      {/if}
       key: {$forum.key} | id={$forum.value.id} | name={$forum.value.name} | parent_id={$forum.value.parent_id}</li>
    {/if}
{/foreach}


    Onto name from variable,
    custom check function,
    loop with section,
    first set to false,
    other name parameter

    Be careful where you start in the array and how many values you output!


Code:
{project array=$forums onto=$forums[4].name check=check_forums,both first=0 name=forum_proj}

total elements: {$forum_proj.total}<br />
levels: {$forum_proj.levels}<br />

<ul>
{section loop=$Support name=id start=8 max=6}
    {if $Support[id].open}
    <ul>
    {elseif $Support[id].close}
    </ul>
    {else}
      <li>
      {if $Support[id].first}
         <i style="color:green">first:</i>
      {/if}
      {if $Support[id].last}
         <i style="color:red">last:</i>
      {/if}
       key: {$Support[id].key} | id={$Support[id].value.id} | name={$Support[id].value.name} | parent_id={$Support[id].value.parent_id}</li>
    {/if}
{/section}
</ul>



I've tested it and it should be fast even with big arrays.
If you have any problems, bugs, questions or suggestions please feel free to post.
_________________
Thinking is the hardest work there is, which is probably the reason why so few engage in it. (Henry Ford)
Back to top
View user's profile Send private message
Display posts from previous:   
This forum is locked: you cannot post, reply to, or edit topics.   This topic is locked: you cannot edit posts or make replies.    Smarty Forum Index -> Plugins All times are GMT
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum


Powered by phpBB © 2001, 2005 phpBB Group
Protected by Anti-Spam ACP