Code snippets for symfony 1.x

Navigation

Snippets by user Francois Zaninotto

One link, multiple ajax div updates, the ultimate solution

One link, multiple ajax div updates, the ultimate solution

I already proposed a solution in another snippet, but it seems I missed the simplest solution.

Let's summarize the probkem again: A page contains one Ajax link, but the remote function must update several divs on the page. Consider, for instance, the following template:

<h1>First zone to update</h1>
<div id="first_zone">
  Hello there
</div>
 
<h1>Second zone to update</h1>
<div id="second_zone">
  <p>How do you do, <strong>mate</strong>?
</div>

We want an ajax link to update the two zones. The solution is not to use the Ajax helpers, but rather call the prototype Ajax object directly:

<?php echo use_helper('Javascript') ?>
<h1>Ajax link</h1>
<?php echo link_to_function('click me', 'new Ajax.Request(\''.url_for('test/ajax').'\');return false')) ?>

The code of the action itself (executeAjax()) does some server stuff to prepare the data used to update the template. It really depends on what logic you put in your Ajax interaction. For this example, it will be empty.

The code of the template (ajaxSuccess.php) just needs to be as follows:

<?php $sf_context->getResponse()->setContentType('text/javascript') ?>
 
<?php slot('first_update') ?>
  So you like clicking, uh?
<?php end_slot() ?>
 
<?php slot('second_update') ?>
  <p>I\'d like to test quotes (like "). </p>
  <p>And <strong>tags</strong>, too.</p>
<?php end_slot() ?>
 
Element.update('first_zone', '<?php include_slot('first_update') ?>'); 
Element.update('second_zone', '<?php include_slot('second_update') ?>');
 

And that's it. Because the response content type is text/javascript, the Ajax object will eval it automatically (no need to mention evalScripts: true anymore).

And this will do exactly what we wanted: update both zones with different content, in a single remote call. The interest of using the slot helpers is that you don't need to worry about escaping the content passed to the JavaScript function, and it looks really nice in your favorite syntax-highlighting text editor.

by Francois Zaninotto on 2007-02-23, tagged ajax 
(5 comments)

package.xml file structure generator for plugins

It can be a hassle to write a package.yml for a plugin if it contains many files. Fortunately, with the help of the sfFinder class and a bit of programming, you can automate the generation of the <content> part in a batch.

Create a batch/package_generator.php with the following code:

<?php
 
define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'prod');
define('SF_DEBUG',       false);
 
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
 
$line          = array();
$lines         = array();
$previous_dirs = array();  
$files         = sfFinder::type('file')->relative()->in('path/to/plugin/files');
 
// Prepare the list of files for nice hierarchy
foreach($files as $file)
{
  $file = str_replace('\\', '/', $file);
  $dirs = explode ('/', $file);
  $line['filename'] = array_pop($dirs);
  $temp_dirs = $dirs;
  foreach($dirs as $dir)
  {
    if($previous_dirs && $dir == $previous_dirs[0])
    {
      array_shift($previous_dirs); 
      array_shift($dirs);
    } 
  }
  foreach($dirs as $dir)
  {
    $line['open'][] = $dir;        
  }
  foreach($previous_dirs as $dir)
  {
    $line['close'][] = $dir;        
  }
  $lines[] = $line;
  $line = array();
  $previous_dirs = $temp_dirs;
}
$blanks = 0;
?>
 
<?php foreach($lines as $line): ?>
<?php if(isset($line['close'])): ?>
<?php foreach($line['close'] as $dir): ?>
<?php echo str_repeat('  ', --$blanks) ?></dir>
<?php endforeach; ?>
<?php endif; ?>
<?php if(isset($line['open'])): ?>
<?php foreach($line['open'] as $dir): $counter++ ?>
<?php echo str_repeat('  ', $blanks++) ?><dir name="<?php echo $dir ?>">
<?php endforeach; ?>
<?php endif; ?>
<?php echo str_repeat('  ', $blanks) ?><file name="<?php echo str_replace('\\', '/', $line['filename']) ?>" role="data"/>
<?php endforeach; ?>

Just run it with

> php batch/package_generator.php

and paste the output in your package.xml. That's all.

by Francois Zaninotto on 2007-01-04, tagged batch  package  plugin 
(2 comments)

One link, multiple ajax div updates, only one remote call

The problem seems to arise quite frequently: a page contains one Ajax link, but the remote function must update several divs on the page. Consider, for instance, the following template:

<h1>First zone to update</h1>
<div id="first_zone">
  Hello there
</div>
 
<h1>Second zone to update</h1>
<div id="second_zone">
  <p>How do you do, <strong>mate</strong>?
</div>
 
<h1>Ajax link</h1>
<?php echo use_helper('Javascript') ?>
<?php echo link_to_remote('click me', array(
  'url'    => 'test/ajax',
  'update' => 'result',
  'script' => true,
)) ?>
<div id="result">
</div>

What would the test/ajax action look like to update both the first_zone and the second_zone?

For the code of the action itself (executeAjax()), we'll ignore it since it really depends on what logic you put in your Ajax interaction. For this example, it will be empty.

The code of the template (ajaxSuccess.php) can be as follows:

<?php echo use_helper('Javascript') ?>
 
<?php slot('first_update') ?>
  So you like clicking, uh?
<?php end_slot() ?>
 
<?php slot('second_update') ?>
  <p>I'd like to test quotes (like "). </p>
  <p>And <strong>tags</strong>, too.</p>
<?php end_slot() ?>
 
<?php echo javascript_tag(
 
  update_element_function('first_zone', array(
    'content' => get_slot('first_update'),
  ))
  .
  update_element_function('second_zone', array(
    'content' => get_slot('second_update'),
  )) 
 
) ?>
 

Once rendered, the HTML code sent to the user will look like this:

<script type="text/javascript">
//<![CDATA[
$('first_zone').innerHTML = '  So you like clicking, uh?\n';
$('second_zone').innerHTML = '  <p>I\'d like to test quotes (like \"). </p>\n  <p>And <strong>tags</strong>, too.</p>\n';
//]]>
</script>

And this will do exactly what we wanted: update both zones with different content, in a single remote call. The interest of using the slot helpers is that you don't need to worry about escaping the content passed to the JavaScript function, and it looks really nice in your favorite syntax-highlighting text editor.

If you happen to do this a lot, maybe you will want to package the multiple updater into a helper. That's quite easy:

function update_elements_function($updates)
{
  $res = "";
  foreach($updates as $zoneName => $slotName)
  {
    $res .= update_element_function($zoneName, array('content' => get_slot($slotName)));
  }
  return javascript_tag($res);
}

Then you could replace the call to the javascript_tag() in the ajaxSuccess.php template by a simpler:

<?php echo update_elements_function(array(
  'first_zone'  => 'first_update',
  'second_zone' => 'second_update',
)) ?>

Performancewise, if the Ajax response is small (below 512 Bytes), you'd better use the JSON approach.

by Francois Zaninotto on 2006-11-22, tagged ajax  helper  multiple 
(3 comments)

One template, multiple themes

Some applications want to offer various look-and-feels for each page. The online documentation already explains how to change the CSS from the server, and a bit of JavaScript will allow you to do it from the client side.

But sometimes this is not enough, and the need arises to have several sets of templates for each action. Something like:

mymodule/
  templates/
    indexSuccess.php    # Template for index action, default theme
    theme1/
      indexSuccess.php  # Template for index action, theme1
    theme2/
      indexSuccess.php  # Template for index action, theme2

Thanks to the MVC architecture, this is very easy to do.

We'll suppose that theme is an attribute of the sfUser object. It contains a simple string. The way to determine the preferred theme for a given user will be left to your sagacity, as well as the way to extend the sfUser class to deliver this attribute. The interesting thing is to modify the view class to have it use the theme when the time comes to look for templates and layouts.

Create a myView.class.php file in the lib/ directory of your application, and write in:

<?php
 
class myView extends sfPHPView
{
  public function configure()
  {
    parent::configure();
 
    // Grab the theme from the user (of from anywhere else)
    $theme = $this->getContext()->getUser()->getTheme();
 
    // If there is a theme and if the theme feature is enabled
    if($theme && sfConfig::get('app_theme'))
    {
      // Look for templates in a $theme/ subdirectory of the usual template location
      if (is_readable($this->getDirectory().'/'.$theme.'/'.$this->getTemplate()))
      {
        $this->setDirectory($this->getDirectory().'/'.$theme);
      }
 
      // Look for a layout in a $theme/ subdirectory of the usual layout location
      if (is_readable($this->getDecoratorDirectory().'/'.$theme.'/'.$this->getDecoratorTemplate()))
      {
        $this->setDecoratorDirectory($this->getDecoratorDirectory().'/'.$theme);
      }
    }
  }
 
}

To force symfony to use this view class instead of the default sfPHPView, create a module.yml in the application's config/ directory, and write in it:

all:
  view_class: my

The new view will look for themes only if you enabled the feature in your app.yml:

all:
  theme: on

Clear the cache, and the theme feature is ready.

It works like this: when a user has a defined theme, symfony will look for templates and layouts in a subdirectory named by this theme. For instance, if a user has a foobar theme, and that it requests the mymodule/myaction action, symfony will look for the template in:

apps/myapp/modules/mymodule/templates/foobar/myActionSuccess.php

and for the layout in:

apps/myapp/templates/foobar/layout.php

The beauty of this modification is that if the themed template doesn't exist, symfony will use the normal template as a fallback.

It must be pretty easy to package this into a plugin, so if you feel like doing it...

by Francois Zaninotto on 2006-11-21, tagged template  theme  view 
(7 comments)

Simulating an enum column type in the model

As enum is a MySQL specific type, you cannot have a column of type enum in your schema.yml (which is database-independent). One solution to simulate it is to create another table to store the different possible values. But the Model class offer an alternative solution that is probably more elegant.

Imagine you have a status column in you article table you want to be an enumerated list with values like (open, closed). Simply declare the status column as integer and then modify the model as follows:

In ArticlePeer.php:

class ArticlePeer...
{
   static protected $STATUS_INTEGERS = array('open', 'closed');
   static protected $STATUS_VALUES = null;
 
   static public function getStatusFromIndex($index)
   {
     return self::$STATUS_INTEGERS[$index];
   }
 
   static public function getStatusFromValue($value)
   {
     if (!self::$STATUS_VALUES)
     {
       self::STATUS_VALUES = array_flip(self::$STATUS_INTEGERS);
     }
 
     $values = strtolower($value);
 
     if (!isset(self::STATUS_VALUES[$value])
     {
       throw new PropelException(sprintf('Status cannot take "%s" as a value', $value));
     }
 
     return self::STATUS_VALUES[strtolower($value)];
   }
}

In Article.php:

class Article
{
   public function setStatusName($value)
   {
     $this->setStatus(ArticlePeer::getStatusFromValue($value));
   }
 
   public function getStatusName()
   {
     return ArticlePeer::getStatusFromIndex($this->getStatus());
   }
}

(Original tip from Fabien)

by Francois Zaninotto on 2006-10-27, tagged enum  getter  model  mysql  setter 
(8 comments)

Form validation in AJAX

The example is for a blog. The page that displays a post also proposes an AJAX form to add a comment. We want that when the validation of this form fails, it displays again in the page with an error message, and when the validation succeeds, the form is replaced byu the comment just posted.

The idea is to take advantage of the way the update option of the form_remote_tag() helper works. It accepts an associative array, where you can specify different zones to update in case of success and failure. The only problem is that for Prototype, a failure is a return code other than 2XX. So when we return the form showing the error message again, we need to set the status code to 404, for instance, for Prototype to choose to update the correct zone.

That, plus the usual use of partials here and there, and you have a working solution:

in modules/post/actions/action.class.php

// Display the form
public function executeShow()
{
  $this->post = PostPeer::retrieveByPk($this->getRequestParameter('post_id'));
}

in modules/post/templates/showSuccess.php

// Display question detail here
...
// Beginning of Comment zone
<div id="added_comment" style="display_none"> </div>
<div id="add_comment">
  <?php include_partial('comment/add_comment', array('post' => $post)) ?>
 </div>

in modules/comment/templates/_add_comment.php

<?php use_helper('Javascript', 'Validation') ?>
<?php echo form_remote_tag(array(
  'url'     => 'comment/add',
  'update'  => array('success' => 'added_comment', 'failure' => 'add_comment'),
  'script'  => true,
  'loading' => "Element.show('indicator')",
  'success' => "Element.hide('indicator');Element.show('added_comment');Element.hide('add_comment');",
  'failure' => "Element.hide('indicator');",
)) ?>
  <?php echo input_hidden_tag('post_id', $post->getId()) ?>
  <?php echo form_error('body') ?>
  <label for="body">Your comment</label>
  <?php echo textarea_tag('body') ?>
  <?php echo submit_tag('Send') ?>
</form>

in modules/comment/validate/add.yml

methods:
  post: [body]
 
fillin:
  activate:        Yes
 
names:
  body:
    required:      Yes
    required_msg:  You must provide a comment
    validators:    spamValidator
 
spamValidator:
   class:          sfRegexValidator
   param:
     match:        No     
     pattern:      /http.*http/
     match_error:  Do not provide more than one URL - It is considered Spam

in modules/comment/actions/action.class.php

public function handleErrorAdd()
{
  $this->post = PostPeer::retrieveByPk($this->getRequestParameter('post_id'));
  $this->getResponse()->setStatusCode(404);
  return sfView::ERROR;
}
 
public function executeAdd()
{
  $post = PostPeer::retrieveByPk($this->getRequestParameter('post_id'));
  $this->forward404Unless($post);
  $comment = new Comment();
  $comment->setPost($post);
  $comment->setAuthor($this->getUser()->getAuthor());
  $comment->setBody($this->getRequestParameter('body'));
  $comment->save();
  $this->comment = $comment;
}

in modules/comment/templates/addError.php

<?php include_partial('comment/add_comment', array('post' => $post)) ?>

in modules/comment/templates/addSuccess.php

Your comment has been added:
<div class="comment">
  <?php echo $comment->getBody() ?>
</div>

As a bonus, the form is still there after a successful submission (but hidden), so with a few more lines of code, you can still provide a Digg-like "edit comment for the next 60 seconds" feature.

by Francois Zaninotto on 2006-10-16, tagged ajax  forms  validation 
(10 comments)

Clearing the cache of another application

This is a bit of a hack, but at least it makes it possible to clear the cache across applications

sfToolkit::clearDirectory(
  sfConfig::get('sf_root_dir').'/cache/'.$app_name.'/templates/path/to/page/in/cache'
);

It is not using an internal URI, of course, since for now an application cannot know another application's routing rules.

If you don't include any path to a page in cache, the whole template cache is cleared.

by Francois Zaninotto on 2006-09-29, tagged cache 

Find durable events between two dates with Propel

The problem is simple. I have a booking system for appartments (or whatever else). Reservations are stored in a Reservation table, and have a begin_date and an end_date column.

Appartment Reservation
id id
... appartment_id
begin_date
end_date
...

I want to find the existing reservations for an appartment between two dates.

The data I have is $appartment_id, $begin_date and $end_date. I want reservations starting or ending between the two dates, or starting before the $begin_date and ending after the $end_date. That's how it would be translated into a SQL WHERE:

reservation.APPARTMENT_ID = $appartment_id 
AND 
(((reservation.START_DATE > $begin_date AND reservation.START_DATE < $end_date) 
  OR 
  (reservation.END_DATE > $begin_date AND reservation.END_DATE < $end_date)) 
  OR 
  (reservation.END_DATE > $end_date AND reservation.START_DATE < $begin_date))

Of course, I'd prefer to use Propel for that. Is it tricky? Not that much.

class Appartment extends BaseAppartment {
 
  public function findReservations($begin_date, $end_date)
  {
    $c = new Criteria();
    $c->add(ReservationPeer::APPARTMENT_ID, $this->getId());
 
    // Find reservations beginning between the search period
    $criterion1 = $c->getNewCriterion(
      ReservationPeer::START_DATE, $begin_date, Criteria::GREATER_THAN
    )->addAnd($c->getNewCriterion(
      ReservationPeer::START_DATE, $end_date, Criteria::LESS_THAN
    ));
 
    // Find reservations ending between the search period
    $criterion2 = $c->getNewCriterion(
      ReservationPeer::END_DATE, $begin_date, Criteria::GREATER_THAN
    )->addAnd($c->getNewCriterion(
      ReservationPeer::END_DATE, $end_date, Criteria::LESS_THAN
    ));
 
    // Find reservations beginning before the search period and ending after
    $criterion3 = $c->getNewCriterion(
      ReservationPeer::END_DATE, $end_date, Criteria::GREATER_THAN
    )->addAnd($c->getNewCriterion(
      ReservationPeer::START_DATE, $begin_date, Criteria::LESS_THAN
    ));
 
    // Combine all that with a OR
    $c->add($criterion1->addOr($criterion2)->addOr($criterion3));
 
    return = ReservationPeer::doSelect($c);
  }
}
by Francois Zaninotto on 2006-09-14, tagged date  propel 
(2 comments)

format_number_choice in JavaScript

If you are dealing with i18n and client side effects, you might need an equivalent for the format_number_choice() helper in Javascript. Here is a simplified version:

function format_number_choice(text_string, replace_hash, number) {
  pattern = new RegExp("\\["+number+"\\]\s?([^\|]*)");
 
  function replace_in_string(text_string, replace_hash) {
    for(var i in replace_hash)
     text_string = text_string.replace(new RegExp(i), replace_hash[i]); 
    return text_string;
  }
 
  matches=text_string.match(pattern);
  if(matches != null)
    return replace_in_string(matches[1], replace_hash);
  else {
    pattern = /\[else\]\s?([^\|]*)/;
    matches=text_string.match(pattern);
    if(matches != null)
      return replace_in_string(matches[1], replace_hash);
    else
      return "not found";
  }
}

Use it in your HTML code as follows:

<script language="JavaScript" type="text/javascript">
  document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", {'Apple' : 'fruit'}, 0))
  document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 1))
  document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 2))
  document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 3))
</script>

This will output:

No fruit
One Apple
Two Apples
Many Apples

Now, just use enclose the first argument with the __() helper, and you have an internationalized format_number_choice() on the client side.

by Francois Zaninotto on 2006-08-29, tagged i18n  javascript 

call the translation helper from an action

You sometimes need to access the __() helper for text translation outside of a template. There are two ways to do it.

You can include the I18NHelper.php manually in your action, or use the symfony command to include a helper (sfLoaded::LoadHElpers()):

sfLoader::LoadHelpers(array('I18N'));

The problem is that this will add the helper functions to the global namespace. Alternately, use the following syntax:

sfContext::getInstance()->getI18n()->__($text, $args, 'messages');

Note: prior to 1.0, you had to do this (now deprecated)

sfConfig::get('sf_i18n_instance')->__($text, $args, 'messages');

(updated 29/11/06)

by Francois Zaninotto on 2006-07-24, tagged helper  i18n 
(3 comments)

Custom criteria for comparing 2 fields from the same record

Imagine that you have a table with the following columns:

MyTable
-------
id
col1
col2

If you want to retrieve the records having col1 greater than col2, the following doesn't work:

$c = new Criteria();
$c->add(MyTablePeer::COL1, MyTablePeer::COL2,  Criteria::GREATER_THAN);

Instead, you need to add a custom condition to your Criteria:

$c = new Criteria();
$c->add(MyTablePeer::COL1, MyTablePeer::COL1.'>='.MyTablePeer::COL2, Criteria::CUSTOM);
by Francois Zaninotto on 2006-07-10, tagged criteria  propel 
(4 comments)

update query using Propel

When you need to update several records in a row, you don't have to loop over the result of a doSelect() call and do a save() for each object (which would make way too many queries).

Instead, you can use the BasePeer method doUpdate() as follows:

BasePeer::doUpdate($select_criteria, $update_criteria, $connection);

For instance:

$con = Propel::getConnection();
 
// select from...
$c1 = new Criteria();
$c1->add(CommentPeer::POST_ID, $post_id);
 
// update set
$c2 = new Criteria();
$c2->add(CommentPeer::RATING, 5);
 
BasePeer::doUpdate($c1, $c2, $con);

If course, if you are in a Peer class, you will need to use the self object:

$con = Propel::getConnection();
 
// select from...
$c1 = new Criteria();
$c1->add(self::POST_ID, $post_id);
 
// update set
$c2 = new Criteria();
$c2->add(self::RATING, 5);
 
BasePeer::doUpdate($c1, $c2, $con);
by Francois Zaninotto on 2006-07-03, tagged criteria  propel 
(8 comments)

Get a date from a culture dependent source

In an i18n application, if your template uses the input_date_tag() helper, the format of the date sent to the submit action will depend on the user culture. But then, how can you handle this date to, say, store it in a database in a culture independent format?

The answer lies in the sfI18N class:

$date= sfContext::getInstance()->getRequest()->getParameter('birth_date');
$user_culture = sfContext::getInstance()->getUser()->getCulture();
list($d, $m, $y) = sfI18N::getDateForCulture(
  $date,
  $user_culture
);

Now you have the day, month and year of the date in the $d, $m and $y variables, and you can do whatever you want with them.

If you use a Propel date setter, you can even call it directly with:

$person->setBirthDate("$y-$m-$d");

The same applies for the sfI18N::getTimestampForCulture() method.

by Francois Zaninotto on 2006-06-23, tagged culture  date  i18n 

Change database connection settings dynamically

To modify the settings of a connection named 'propel':

$con = sfContext::getInstance()->getDatabaseConnection('propel');
$con->setConnectionParameter('username', 'foo');
$con->setConnectionParameter('password', 'bar');

This works for all the settings that can be defined in the databases.yml (more info in the sfPropelDatabase class source).

You must execute this code before the first query to the database. If you do that after a first query, it fails. This means that the best way to implement it is in a filter.

by Francois Zaninotto on 2006-06-20, tagged connection  database  propel 
(1 comment)

Query objects between two dates with Criteria

Say you look for the objects of class Foo being created between $from_date and $to_date. This should do the trick:

$c = new Criteria();
$criterion = $c->getNewCriterion(FooPeer::CREATED_AT , date('Y-m-d', $from_date), Criteria::GREATER_EQUAL  );
$criterion->addAnd($c->getNewCriterion(FooPeer::CREATED_AT , date('Y-m-d', $to_date), Criteria::LESS_EQUAL ));
$c->add($criterion);
$shows = FooPeer::doSelect($c);
by Francois Zaninotto on 2006-05-24, tagged criteria  date  propel 
(6 comments)

Call the command line via the browser

You can mimick the call of the symfony command line via a PHP script called from the browser. For instance, for the command

$ symfony clear-cache

...create a webclearcache.php file in your myproject/web/ directory (where the index.php is) with the following content:

<?php
 
// as we are in the web/ dir, we need to go up one level to get to the project root
chdir(dirname(__FILE__).DIRECTORY_SEPARATOR.'..');
 
include_once('/lib/symfony/pake/bin/pake.php');
 
$pake = pakeApp::get_instance();
try
{
  $ret = $pake->run('/data/symfony/bin/pakefile.php', 'clear-cache');
}
catch (pakeException $e)
{
  print "<strong>ERROR</strong>: ".$e->getMessage();
}
 
?>

You can now clear the cache by calling:

http://myapp.example.com/webclearcache.php

Note: Beware that by letting web access to administration tools, you can compromise the safety of your website.

by Francois Zaninotto on 2006-05-24, tagged cli  pake 
(3 comments)

Call the command line without a pear install

If you have not a PEAR symfony installation, but rather a couple of symlinks in the lib/ and data/ directories of your project, and if you didn't define shortcuts for the symfony command, you'll have to call manually pake (the tool used for the command line interface) and pass it the right pakefile (the configuration file containing the symfony commands).

This can be done that way:

$ cd /home/production/myproject
$ php /path/to/pake.php -f data/symfony/bin/pakefile.php propel-build-model

In systems where both PHP4 and PHP5 are installed, you probably need to use a PHP5 specific command:

$ php5 /usr/local/php5.1.2/lib/php/pake.php -f data/symfony/bin/pakefile.php propel-build-model
by Francois Zaninotto on 2006-05-23, tagged cli  pake 
(1 comment)

AJAX vote - 5 stars

In the template where the object that users can vote on (here, a snippet) is displayed, add:

  <?php echo use_helper('Javascript') ?>
  <?php $id = $snippet->getId() ?>
  <?php if($sf_user->canVoteFor($snippet)): ?>
    <span id="vote_for_<?php echo $id ?>" style="white-space:nowrap;">
      rate this snippet: <?php for($i = 1 ; $i <= 5 ; $i++): ?><div class="rate <?php if($i <= $snippet->getAverageVote()): ?>rated<?php endif; ?>" 
      id="vote_<?php echo $id ?>_<?php echo $i ?>"><?php echo link_to_remote(image_tag('spacer.gif', 'width=10 height=10'), array(
        'url'         => 'snippet/voteForSnippet?id='.$id.'&vote='.$i,
        'update'      => 'vote_for_'.$id,
        'loading'     => visual_effect('appear', 'indicator'),
        'complete'    => visual_effect('fade', 'indicator').visual_effect('highlight', 'vote_for_'.$id),
      ), array(
        'onMouseOver' => 'highlight_stars('.$id.', '.$i.', true);',
        'onMouseOut'  => 'highlight_stars('.$id.', '.$i.', false);',  
      )) ?></div><?php endfor; ?>
      <span id="indicator" style="display:none"> <?php echo image_tag('indicator.gif') ?></span>
    </span>
  <?php else: ?>
    <?php include_partial('voted', array('id' => $id, 'vote' => $snippet->getAverageVote())) ?>
  <?php endif; ?>

Of course, you have to define the rules about who can vote on what in the -&gt;canVoteFor() method of the User object (in apps/myapp/lib/myUser.php). The template uses a _voted.php partial:

<?php for($i = 1 ; $i <= 5 ; $i++): ?><div class="rate <?php if($i <= $vote): ?>rated<?php endif; ?>" id="vote_<?php echo $id ?>_<?php echo $i ?>"><img src="/images/spacer.gif" width="10" height="10" /></div><?php endfor; ?>

DO NOT add extra spaces to the long lines, or the stars get separated by blank spaces.

The mechanism that changes the aspect of stars relies on classes, so add the following styling to a CSS used in your template:

.rate {
display:inline;
width:10px;
height:10px;
margin:0;
padding:0;
background-image:url(/images/vote_star.gif);
background-position: left 10px;
}
.rated
{
background-position: left 0px;
}
.ratehover {
background-position: left 20px;
}

The vote_star.gif file is a 10x30px image containing three versions of the star: voted, hovered, not voted.

vote_star

The snippet/voteForSnippet action does something like:

  public function executeVoteForSnippet()
  {
    $this->id   = $this->getRequestParameter('id');
    $snippet = SnippetPeer::retrieveByPk($this->id);
    if(!$snippet)
    {
      return sfView::NONE;
    }
 
    $vote = new Vote();
    $vote->setUserId($this->getUser()->getUserId());
    $vote->setSnippetId($snippet->getId());
    $vote->setVote($this->getRequestParameter('vote'));
    $vote->save();
 
    $this->vote = $vote->getSnippet()->getAverageVote();
  }
by Francois Zaninotto on 2006-05-20, tagged ajax  vote 
(7 comments)

Pagination navigation helper

In a template displaying a paginated list, you need to show the pager navigation. Create a PaginationHelper.php in lib/helper:

<?php
 
function pager_navigation($pager, $uri)
{
  $navigation = '';
 
  if ($pager->haveToPaginate())
  {  
    $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';
 
    // First and previous page
    if ($pager->getPage() != 1)
    {
      $navigation .= link_to(image_tag('/sf/images/sf_admin/first.png', 'align=absmiddle'), $uri.'1');
      $navigation .= link_to(image_tag('/sf/images/sf_admin/previous.png', 'align=absmiddle'), $uri.$pager->getPreviousPage()).' ';
    }
 
    // Pages one by one
    $links = array();
    foreach ($pager->getLinks() as $page)
    {
      $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);
    }
    $navigation .= join('  ', $links);
 
    // Next and last page
    if ($pager->getPage() != $pager->getLastPage())
    {
      $navigation .= ' '.link_to(image_tag('/sf/images/sf_admin/next.png', 'align=absmiddle'), $uri.$pager->getNextPage());
      $navigation .= link_to(image_tag('/sf/images/sf_admin/last.png', 'align=absmiddle'), $uri.$pager->getLastPage());
    }
 
  }
 
  return $navigation;
}

In your templates, display the pagination links like that:

<?php echo use_helper('Pagination') ?>
<?php echo pager_navigation($mypager, '@myrule') ?>
by Francois Zaninotto on 2006-05-20, tagged helper  pager  pagination  propel 

Load data batch

Create a batch/load_data.php script with:

<?php
 
define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG',       true);
 
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
 
// initialize database manager
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
 
$data = new sfPropelData();
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
 
?>

Call it with:

$ cd batch
$ php load_data.php

It will load the data contained in all the .yml files of the data/fixtures/ directory.

by Francois Zaninotto on 2006-05-20, tagged batch  data  propel 
(4 comments)