Code snippets for symfony 1.x

Navigation

Snippets by user scott meves

Pager for an array of elements v.2

Inspired by this snippet (http://www.symfony-project.com/snippets/snippet/83) here is a version I created that inherits from sfPager. I had a collection of objects (not propel objects) that I needed to iterate through and this did the trick. Note that this is not really optimal because the full array has to be populated with whatever data you are looking at during each request, whereas something like sfPropelPager is smart enough to only fetch/hydrate the objects you are going to need for that particular view.

action:

$myArrayOfThings = array('first', 'second', 'and so on');
$this->pager = new myArrayPager(null, 15);
$this->pager->setResultArray($myArrayOfThings);
$this->pager->setPage($this->getRequestParameter('page',1));
$this->pager->init();

the class:

<?php
class myArrayPager extends sfPager
{
  protected $resultsArray = null;
 
  public function __construct($class = null, $maxPerPage = 10)
  {
    parent::__construct($class, $maxPerPage);
  }
 
  public function init()
  {
    $this->setNbResults(count($this->resultsArray));
 
    if (($this->getPage() == 0 || $this->getMaxPerPage() == 0))
    {
     $this->setLastPage(0);
    }
    else
    {
     $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage()));
    }
  }
 
  public function setResultArray($array)
  {
    $this->resultsArray = $array;
  }
 
  public function getResultArray()
  {
    return $this->resultsArray;
  }
 
  public function retrieveObject($offset) {
    return $this->resultsArray[$offset];
  }
 
  public function getResults()
  {
    return array_slice($this->resultsArray, ($this->getPage() - 1) * $this->getMaxPerPage(), $this->maxPerPage);
  }
 
}
by scott meves on 2007-05-08, tagged pager  sfpager 
(3 comments)

Navigation Jump Menu Drop Down

If you want to have a quick drop down (select) menu that automatically takes users to a specific page on your site, this method works well and results in clean URLS. This snippet is for content / urls stored in a database.

There may be a better way to do this, so suggestions are welcome!

First, set up a route like this:

show_content:
  url: content/:title
  params: { module: content, action: show }

That way we can have urls like "content/about" or "content/news". (Note you will want to make sure your content titles are URL friendly themselves, perhaps using a stripped_title field instead of the title field that is parsed to replace spaces and punctuation.)

In the peer class of the "content" table we want to create a method we can use to populate our jump menu:

public static function getUrlOptions()
{
  $options = array();
  $c = new Criteria();
  $c->addAscendingOrderByColumn(self::NAME);
  $contents = ContentPeer::doSelect($c);
 
  foreach ($contents as $content) {
    $url = sfContext::getInstance()->getController()->genUrl('content/show?id='.$content->getId());
    $options[$url] = $content->getTitle();
  }
 
  return $options;
}

in the template we rely on those options and create a select menu with a javascript behavior:

Jump to: <?php echo select_tag('title', options_for_select(ContentPeer::getUrlOptions(), $currentContentUrl), array('onchange'=>'window.document.location = this.value')) ?>

And finally, in the action we store the current url so we can have it pre-selected in the jump menu on the subsequent page:

    $this->content = ContentPeer::retrieveByTitle($this->getRequestParameter('title'));
    $this->currentContentUrl = $this->getController()->genUrl(sfRouting::getInstance()->getCurrentInternalUri());

You now end up with a select menu like this:

<select name="category" onchange="...">
  <option value="/content/news">News</option>
  <option value="/content/sports">Sports</option>
  ...
</select>

Selecting an option from the list will redirect your visitor to the URL stored as the option value.

by scott meves on 2007-04-09, tagged jump  menu  select 

Credit Card Validator II

Here is another variation on the credit card validator. In your validate/[action].yml file, you can implement this helper like so:

fields:
  cc_type:
    required:
      msg:      Please select a card type
    sfStringValidator:
      values:       [Visa, MasterCard, Discover, American Express]
      values_error: Please select a credit card type
      insensitive:  true
 
  cc_number:
    required:
      msg:      Please provide a credit card number
    myCreditCardValidator:
      card_name: cc_type    # refers to field name in form that contains card type, like Visa, MasteCard, etc.

Place this file, myCreditCardValidator.class.php, in you application's /lib directory and clear the cache.

<?php
 
/**
 * This class has been converted to a Symfony Validator from original code
 * created by John Gardner, 4th January 2005. 
 * http://www.braemoor.co.uk/software/index.shtml
 *
 * Symfony conversion by Scott Meves, Stereo Interactive & Design, 2007
 * http://www.stereointeractive.com
 * 
 * This routine checks the credit card number. The following checks are made:
 * 
 * 1. A number has been provided
 * 2. The number is a right length for the card
 * 3. The number has an appropriate prefix for the card
 * 4. The number has a valid modulus 10 number check digit if required
 * 
 **/
 
class myCreditCardValidator extends sfValidator
{    
 
    static protected $CARDS = array (
        array ('name'           => 'American Express', 
            'length'            => '15', 
            'prefixes'      => '34,37',
            'checkdigit'    => true
    ),
        array ('name'           => 'Carte Blanche', 
               'length'         => '14', 
               'prefixes'   => '300,301,302,303,304,305,36,38',
               'checkdigit' => true
        ),
        array ('name'       => 'Diners Club', 
               'length'         => '14',
               'prefixes'   => '300,301,302,303,304,305,36,38',
               'checkdigit' => true
    ),
        array ('name'       => 'Discover', 
               'length'         => '16', 
               'prefixes'   => '6011',
               'checkdigit' => true
    ),
        array ('name'       => 'Enroute', 
               'length'         => '15', 
               'prefixes'   => '2014,2149',
               'checkdigit' => true
    ),
        array ('name'       => 'JCB', 
               'length'         => '15,16', 
               'prefixes'   => '3,1800,2131',
               'checkdigit' => true
    ),
        array ('name'       => 'Maestro', 
               'length'         => '16', 
               'prefixes'   => '5020,6',
               'checkdigit' => true
    ),
        array ('name'       => 'MasterCard', 
               'length'         => '16', 
               'prefixes'   => '51,52,53,54,55',
               'checkdigit' => true
    ),
        array ('name'       => 'Solo', 
               'length'         => '16,18,19', 
               'prefixes'   => '6334, 6767',
               'checkdigit' => true
    ),
        array ('name'       => 'Switch', 
               'length'         => '16,18,19', 
               'prefixes'   => '4903,4905,4911,4936,564182,633110,6333,6759',
               'checkdigit' => true
    ),
        array ('name'       => 'Visa', 
               'length'         => '13,16', 
               'prefixes'   => '4',
               'checkdigit' => true
    ),
        array ('name'       => 'Visa Electron', 
               'length'         => '16', 
               'prefixes'   => '417500,4917,4913',
               'checkdigit' => true
        )
  );
 
  public function initialize($context, $parameters = null)
  {
    // initialize parent
    parent::initialize($context);
 
    // set defaults
    $parameterHolder = $this->getParameterHolder();
    $parameterHolder->set('cc_error_type',      'Unknown card type');
    $parameterHolder->set('cc_error_missing', 'No card number provided');
    $parameterHolder->set('cc_error_format',    'Credit card number has invalid format');
    $parameterHolder->set('cc_error_number',    'Credit card number is invalid');
    $parameterHolder->set('cc_error_length',    'Credit card number is wrong length');
 
    $this->getParameterHolder()->add($parameters);
 
    return true;
  }
 
  public function execute(&$value, &$error)
  {
    $cardName = $this->getParameterHolder()->get('card_name');
    $cardName = $this->getContext()->getRequest()->getParameter($cardName);
 
    $cardNumber = $value;
 
      // Establish card type
      $cardType = -1;
      for ($i=0; $i<sizeof(self::$CARDS); $i++) 
        {   // See if it is this card (ignoring the case of the string)
        if (strtolower($cardName) == strtolower(self::$CARDS[$i]['name']))
            {
          $cardType = $i;
          break;
        }
      }
 
      // If card type not found, report an error
      if ($cardType == -1) 
        {
            $error = $this->getParameterHolder()->get('cc_error_type');
        return false; 
      }
 
      // Ensure that the user has provided a credit card number
      if (strlen($cardNumber) == 0)
        {
         $error = $this->getParameterHolder()->get('cc_error_missing');
         return false; 
      }
 
      // Remove any non-digits   from the credit card number
      $cardNo = preg_replace('/[^0-9]/', '', $cardNumber);
 
      // Check that the number is numeric and of the right sort of length.
      if (!eregi('^[0-9]{13,19}$',$cardNo))
        {
         $error = $this->getParameterHolder()->get('cc_error_format');
         return false; 
      }
 
      // Now check the modulus 10 check digit - if required
      if (self::$CARDS[$cardType]['checkdigit'])
        {
        $checksum = 0;   // running checksum total
        $mychar = "";    // next char to process
        $j = 1;          // takes value of 1 or 2
 
        // Process each digit one by one starting at the right
        for ($i = strlen($cardNo) - 1; $i >= 0; $i--) 
            {
          // Extract the next digit and multiply by 1 or 2 on alternative digits.      
          $calc = $cardNo{$i} * $j;
 
          // If the result is in two digits add 1 to the checksum total
          if ($calc > 9) {
            $checksum = $checksum + 1;
            $calc = $calc - 10;
          }
 
          // Add the units element to the checksum total
          $checksum = $checksum + $calc;
 
          // Switch the value of j
          if ($j ==1) {$j = 2;} else {$j = 1;};
        } 
 
        // All done - if checksum is divisible by 10, it is a valid modulus 10.
        // If not, report an error.
        if ($checksum % 10 != 0)
            {
            $error = $this->getParameterHolder()->get('cc_error_number');
            return false; 
        }
      }  
 
      // The following are the card-specific checks we undertake.
 
      // Load an array with the valid prefixes for this card
      $prefix = split(',',self::$CARDS[$cardType]['prefixes']);
 
      // Now see if any of them match what we have in the card number  
      $prefixValid = false; 
      for ($i=0; $i<sizeof($prefix); $i++)
        {
        $exp = '^' . $prefix[$i];
        if (ereg($exp,$cardNo))
            {
          $prefixValid = true;
          break;
        }
      }
 
      // If it isn't a valid prefix there's no point at looking at the length
      if (!$prefixValid)
        {
         $error = $this->getParameterHolder()->get('cc_error_number');
         return false; 
      }
 
      // See if the length is valid for this card
      $lengthValid = false;
      $lengths = split(',',self::$CARDS[$cardType]['length']);
      for ($j=0; $j<sizeof($lengths); $j++)
        {
        if (strlen($cardNo) == $lengths[$j])
            {
          $lengthValid = true;
          break;
        }
      }
 
      // See if all is OK by seeing if the length was valid. 
      if (!$lengthValid)
        {
         $error = $this->getParameterHolder()->get('cc_error_length');
         return false; 
      };   
 
      // The credit card is in the required format.
      return true;
  }
}
by scott meves on 2007-04-08, tagged cc  creditcard  validation  validator 

create styled navigation for current view / highlight active link v.2

I have rewritten my original link_to_styled helper to accommodate more common scenarios. This helper will append a class name to the link if the current route matches the one present in the link.

You can specify within the $style_options parameter to match only the module name rather than the entire route. This is useful for top-level navigation links that correspond to your modules.

You can also specify a tag that will encapsulate your link and receive the class name rather than the <a> tag if the active-state requirements are met.

Additionally, any existing class names given to your links are maintained rather than overwritten with the active class name.

As others have suggested, I'm sure it could still be improved to allow for partial route matching, perhaps excluding query parameters.

NOTE: Thanks to Fabian Lange's suggestion, I reordered the parameters so that the style parameters come before the less-used html options.

Enjoy!

in lib/helper/StyleHelper.php:

/**
 * Automatically adds a class name to the link if the link route matches
 * the current module or route.
 *
 * <b>Style Options:</b>
 * - 'class' - the class name to append to the link, defaults to 'current'
 * - 'module_only' - append class name if the current module and link module match
 * - 'tag' - an html to encapsulate the link, which will receive the class name rather than the <a> tag
 *
 * <b>Examples:</b>
 * <code>
 *   echo link_to_styled('Events', 'event/list', 'class=active', 'id=event_link', );
 *   if the current route is 'event/list', then:
 *     => <a href="path/to/event/list/action" id="event_link" class="active">Events</a>
 * 
 *   echo link_to_styled('Events', 'event/list', 'class=active module_only=true tag=li', 'id=event_link');
 *   if the current module matches the module present in the route, then:
 *     => <li class="active"><a href="path/to/event/list/action" id="event_link">Events</a></li>
 * </code>
 *
 * @param  string name of the link, i.e. string to appear between the <a> tags
 * @param  string 'module/action' or '@rule' of the action
 * @param  array additional style options
 * @param  array additional HTML compliant <a> tag parameters
 * @return string XHTML compliant <a href> tag
 * @see    link_to
 */
function link_to_styled($text, $route = '', $style_options = '', $options = '') {
 
    static $context, $current_module_name;
 
    $options = _parse_attributes($options);
    $style_options = _parse_attributes($style_options);
 
    if (!isset($style_options['class']))
    {
        $style_options['class'] = 'current';
    }
 
    if (isset($style_options['module_only']))
  {
        if (!isset($context))
        {
            $context = sfContext::getInstance();
        }
 
        if (!isset($current_module_name))
      {
        $current_module_name = $context->getModuleName();
      }
 
    list($current_route_name, $params) = $context->getController()->convertUrlStringToParameters($route);
        $is_current = $params['module'] == $current_module_name;
  } 
    else 
    {
        $current_route = sfRouting::getInstance()->getCurrentInternalUri();
        $is_current = $current_route == $route;
    }
 
    if (isset($style_options['tag']))
    {
        $tag_options = (true === $is_current) ? array('class'=>$style_options['class']) : array();
        $return_string = content_tag($style_options['tag'], link_to($text, $route, $options), $tag_options);
    }
    else
    {   
        if (true === $is_current)
        {
            $options['class'] = isset($options['class']) ? $options['class'] . ' ' . $style_options['class'] : $style_options['class'];
        }
        $return_string = link_to($text, $route, $options);
    }
 
    return $return_string;
}

and in the template:

<?php use_helper('Style') ?>
<ul id="navlist">
<!-- highlight this link if we are within the "events" module -->
<li><?php echo link_to_styled('My Events', 'events/list', 'module_only=true') ?></li>
</ul>
 
<!-- or you could add the style to the li tag -->
<?php echo link_to_styled('Event List', 'events/list', 'tag=li class=active') ?></li>
by scott meves on 2007-02-24, tagged css  helper  linkto  navigation 
(6 comments)

Recalling AJAX state upon page reload

Remembering AJAX State

If you are updating your interface with ajax actions, typically users have no way to bookmark or return to the page with those updates already in place. This is seen as a fundamental problem with ajax: the URL shown at the top of the browser no longer constitutes a complete specification of the information shown in the window.

This snippet explains how you can track and recreate the current view even when using ajax to update your page. Now if users reload the page or send this link to a friend, the current state will remain intact. (You can view an example of this technique on http://www.ask.com/)

The trick relies on two parts:

  1. Updating the window.location.hash (the part of the url after the "#", also called an anchor) on a page.
  2. Using it to reconstruct the state of the interface. In other words, when users click on your ajax links, you will update the URL with a new hash string, and when the page is loaded you can you look for this hash string and update the interface accordingly.

Part One: Tracking State

First, you must allow the links created using the helpers link_to_remote and link_to_function to NOT have a 'return false;' appended to the end of the onclick attribute of the tag that is generated. There is no way to change this by way of the default helper, but it's easy to override with our own.

A 'return false' statement means the browser won't try to navigate to the url in the 'href' attribute of your link. Usually you don't want your browser window to navigate to the default "href" link--you are just creating the link as means to make the ajax call. Typically href attributes, if any, are present in case the user does not have javascript enabled.

However, we do not want "return false" appended to the onclick attribute. Ideally our helper function would allow us to do something like this:

<?php echo my_link_to_remote('Read this post', array(
    'update' => 'indicator',
    'url'    => 'post/read?id='.$post->getId(),
    'return' => 'true',
), array('href' => '#post:read|id:'.$post->getId()) ?>

which creates this:

<a href="#post:read|id:1234" onclick="new Ajax.Updater(....); return true;">Read this post</a>

When a user clicks the link, not only is the ajax action called but the URL of the page gets updated to something like "http://mysite.com/#post:read|id:1234".

So, let's write our own customer helper. I placed it into the /lib/helper directory in a file called myJavascriptHelper.php.

<?php
/* File: myJavascriptHelper.php */
 
/**
 * Returns a link that will trigger a javascript function using the
 * onclick handler and return TRUE OR FALSE after the fact 
 * depending on the $html_options['return'] parameter. 
 * This attribute is removed before we use the default content_tag helper.
 *
 * Examples:
 *   <?php echo link_to_function('Greeting', "alert('Hello world!')", array('return'=>true)) ?>
 *   <?php echo link_to_function(image_tag('delete'), "if confirm('Really?'){ do_delete(); }") ?>
 */
function my_link_to_function($name, $function, $html_options = array())
{
  $html_options = _parse_attributes($html_options);
 
  $html_options['href'] = isset($html_options['href']) ? $html_options['href'] : '#';
  $html_options['onclick'] = $function.'; return ';
  $html_options['onclick'] .= (isset($html_options['return']) && $html_options['return'] == true) ? 'true;' : 'false;';
  unset($html_options['return']);
 
  return content_tag('a', $name, $html_options);
}
 
/**
 * See docs on link_to_remote helper for more info.
 */
function my_link_to_remote($name, $options = array(), $html_options = array())
{
  return my_link_to_function($name, remote_function($options), $html_options);
}
 
?>

Now for my ajax links I use my new helper and make sure to pass an href attribute as well as the 'return' attributte. This determines if clicking on the link will return true (update the url) or false (don't update it). Here is a real example of how I use it to keep track of what message is currently viewed in a message component within my interface.

<?php echo my_link_to_remote('Load This Message Inline', 
    array(
        'update'    => 'message_viewer',
        'url'=>'message/viewInline?id='.$message->getId().'&view='.$view,
        'loading' => "Element.show('message_indicator')",
        'complete'=> "viewInlineComplete(".$message->getId().", '".$view."')",
        'method'=>'get',
    ), array(
        'href' => '#message:viewInline|id:'.$message->getId().'|view:'.$view,
        'return' => true)) ?>

Notice that the href attribute has a string that mirrors the URL attribute. We could probably take the my_link_to_remote helper a step further and automatically generate the HREF attribute based on the URL, but I decided to keep some flexibility there. Now, if a user clicks the link, the url will get a string like "#message:viewInline|id:3|view:inbox" appended to it.

In my experience, both IE 6 and FF did NOT scroll to the top of the window nor did the page reload. In fact I believe the W3 DOM specs state that links to a hash address never reload the document. Instead, I get a nice new attribute in the URL of the page. Now, if my user wants to bookmark the page, or perhaps reload it, the current ajax state can be recalled using the information in the anchor tag.

Part Two: Using the Hash to Reconstruct State

Although you cannot retrieve the value after the hash from within PHP, you can retrieve it client-side by way of the 'location.hash' function and then set a cookie, update the page, or do whatever you like based on those attributes. I am using it to reload a message from a users inbox into the inline viewer. Let's see how it works.

First, we need a way to load in the hash string and turn it into something we can use. Here is the function I use and included in my main.js file that is used throughout my site.

function readHashVars()
    {
        var hash = window.location.hash;
        hash = hash.substring(1);
        hash = hash.replace(/\|/g, '&');
        hash = hash.replace(/\:/g, '=');
        // lets turn our url hash into a javascript hash (like an associated array)
        // we will use a useful function provided in the prototype library
        hash = hash.parseQuery();
        return hash;
    }

Note: This function requires the prototype library to work!

Next, whenever my page is loaded I should check to see if the hash contains attributes I set within my ajax actions. If there are, I call the same client-side update from remote function as I do when a user clicks on the ajax link in the interface. Here is the top of my indexSuccess.php file for my home page action. I do not include this within the main.js file because I want to use the PHP helpers like remote_function provided by symfony. Compare this to the use of my_link_to_remote in the code above.

<?php use_helper('Javascript') ?>
<?php echo javascript_tag("
function updateDashboard()
{
    var hash = readHashVars();
    // we now have a hash with attributes matching the parameters in the url
    // so if the url has #message:viewInline|id:3, we have a javascript object like
    // hash { message: viewInline, id: 3 }
 
    if(hash.message == 'viewInline') {
        " . remote_function(array(
            'update'    => 'message_viewer',
            'url'           => 'message/viewInline',
            'with'      => "'id='+hash.id+'&view='+hash.view",
            'loading' => "Element.show('message_indicator')",
            'complete'=> 'viewInlineComplete(hash.id, hash.view)',
            'method'    => 'get',
        )) . "
    }
}
 
Event.observe(window, 'load', updateDashboard, false);
") ?>

This time, instead of calling the "remote_function" helper with parameters within PHP, I rely on javascript to send the correct parameters using the "with" parameter. That way, I can include values from javascript that aren't available until the page is loaded.

That's all there is to it (phew!). Now that this is all set up, I can easily convert my inline ajax links to javascript functions that are automatically called when the necessary attributes exist within the URL hash.

by scott meves on 2007-01-29, tagged ajax  linktoremote 

create styled navigation for current view

Often times in my navigation I want to change the style of the "current" tab, link, menu item, etc. to reflect that the user is in fact on that page. In order to do this I created a super simple helper called link_to_styled. I added this code to app/lib/helper/StyleHelper.php:

function link_to_styled($text, $route = '', $options = '', $class_name = 'current') {
    $current_route = sfRouting::getInstance()->getCurrentInternalUri();
    if($route == $current_route) {
        if(is_array($options)) {
            $options['class'] = $class_name;
        } else {
            $options .= ' class='.$class_name;
        }
    }
    return link_to($text, $route, $options);
}

and then in my global navigation template I do something like this:

<?php use_helper('Style') ?>
<ul id="navlist">
<li><?php echo link_to_styled('My Friends', 'friends/list') ?></li>
<li><?php echo link_to_styled('My Clubs', 'clubs/list') ?></li>
<li><?php echo link_to_styled('My Events', 'events/list')?></li>
</ul>

The helper will automatically add a class='current' to the link tag. This function could be made more robust, but for a quick and simple solution it seems to do the trick.

by scott meves on 2006-11-02, tagged css  helper  navigation 
(2 comments)