php

Conveniently Programming RSS Feeds in Drupal

Drupal feeds most of the content out of the box. If it doesn't do what you want, it allows you to use format_rss_item() and format_rss_channel(). This stuff is simple to use, but not very convenient by itself. There are two things that I like to do when building feeds:

  • Use theme functions to format items. (Makes designer happy.)
  • Make it as convenient as possible to program new types of RSS items (hello, PHP Metaprogramming).

I should mention that I'm working with Drupal 5.7, but any 5.x -6.x shouldn't be much different.

What's with that second point?

Yes, I like convenience. Well, say you're using a feed for getting updates on what your users do on your website. Sort of - an administration feed. Don't know about you, but I would like to have a nice little function that I can call anywhere in my module, give it all the data, and forget it. Say, on update of some certain content type I wanna post a notice to the admin feed. All I have to do is call one convenient function right there, and relax with Corona, or Guinness if it's winter.

Step 1. Preparing the RSS items to be published.

Let's create a table admin_feed that's gonna hold the feed items. We need 4 columns.

  • id - an integer (index)
  • type - a string (varchar possibly)
  • data - text
  • created - datetime/timestamp (should default to NOW)

Now let's open our module foo and add that single convenient function I was talking about. When I want to feed something, I want to specify what type of action is happening, and throw in some data as an object. This function will take my type, serialize my object, and save it to the db. Should be something like this:

<?php
   
function foo_feed_item($type, &$data = null) {
       
db_query("insert into admin_feed (type, data) values ('%s', '%s')", $type, serialize($data));
    }
?>

Alrighty. This was all the work necessary to get the feed items to save somewhere. Now the fun begins.

Step 2. Outputting feed.

Now it's time to display the feed. There should be a menu item that creates a path to our rss feed.

<?php
            $items
[] = array(    'path' => 'rss/admin',
                           
'title' => t('Admin Infostream'),
                           
'callback' => 'foo_feed_admin',
                           
'access' => true,
                           
'type' => MENU_CALLBACK
           
);
?>

This path expects the callback function foo_feed_admin() to exist, so lets create it. In the function we should take all of those saved items from admin_feed db table, and display them as a formatted feed. Here's my implementation, although you would probably want to make this feed private, and require a login:

<?php
   
function foo_feed_admin() {
       
$query = db_query('select * from admin_feed order by created desc');
       
$items = '';
       
        while (
$r = db_fetch_object($query))
           
$items .= theme('admin_feed', $r);
       
       
header(theme('feed_header'));
        echo
theme('feed_wrapper', $items);
    }
?>

I should mention two things about the above function. First of all, look at that loop. It goes through every item in the database, and passes it to the theme('admin_feed') function. We haven't written it yet, but getting there. Second of all, I have theme('feed_header') and theme('feed_wrapper') for my purposes, but you can make your own theme functions for that. It's important to see that I'm setting the header and using "echo" to output the result. This result is what you will see when you go to "rss/admin" path.

On to the theme now!

I will call my theme bar. So let's go to the sites/all/themes/bar directory. I have a template.php file there. Sometimes template.php grows too large with many theming functions for different things. This is why I prefer to split it into multiple files. Thus, for admin feed I created a file called theme_admin_feed.inc and included it into the template.php, but that's up to you. Now, let's create the first function - the one that will produce an RSS item.

<?php
   
function _make_item($title, $body, $link, $id) {
        return
format_rss_item(
           
$title,
           
$link,
           
$body,
            array(
               
'author' => 'nomail@something.com (admin)',
               
'guid' => $id
           
)
        );
    }
?>

This function returns a formatted item. Nothing to see here. You will see how we use it later.

The next function, however, will be interesting. This is the bar_admin_feed() function that we're calling from the module and passing the database rows to it. Here's what it looks like:

<?php
   
function bar_admin_feed( &$r ) {
        return
call_user_func_array('_af_'.$r->type, array( unserialize($r->data),  '['. strtoupper($r->type) .']', $r->id) );
    }
?>

This function uses php metaprogramming to call other functions in this file, depending on the type of the feed item. For example, if $r->type is "update", it will call _af_update() and pass those three parameters to it. I prefix '_af_' to prevent collisions with other functions in my template.php. It's up to you, but here "af" stands for "admin feed". Also I like to prefix all the titles of my admin feed with an uppercase type, wrapped into square brackets. So I would have something like "[UPDATE] User Johnny updated node 34". This helps if you want to apply coloring rules in your RSS reader later. So that's why I'm passing '['. strtoupper($r->type) .']' into my function. Now it's time to show how easy it has become to actually add those Update, Delete, Create notices to your program.

Step 3. Posting RSS items is now a piece of cake!

So now I want to post a feed item every time someone updates a node in Drupal, including the user who made the update, and whatever else I desire. I have my foo_nodeapi() where I catch the update action, the $node, etc. Here's what I'll do in there:

<?php
           
case 'update':
                global
$user;
               
$node->bywhom = $user;
               
foo_feed_item('update', $node);
                unset(
$node->bywhom);
            break;
?>

In this example I'm piggybacking the $user onto the $node, and passing it to foo_feed_item(). You could piggyback anything you want this way.

Now in our theme template.php (or in my case theme_admin_feed.inc) we have to write the theming function for this 'update' action. Here's how mine looks:

<?php
   
function _af_update(&$node, $title, $id) {
       
$title .= "$node->title was updated by $node->bywhom->name";
       
$body = "<a href='http://something.com/node/$node->nid/edit'>
                http://something.com/node/$node->nid/edit
                </a>
        "
;
       
$link = "http://something.com/node/$node->nid/edit";
       
        return
_make_item($title, $body, $link, $id);
    }
?>

As you can see, I have all the info conveniently provided to me. This is also the place where we're finally using that _make_item() function we wrote earlier. So I format my item anyway I want and pass it to _make_item(). I give myself some convenient links to the updated node, and the name of the person who made the update. All nice and tidy.

A few more words.

I'm sure there are tons of ways to accomplish this in a convenient way, and I would like to hear about them. If you see a better way to do something, a more clever technique, or anything, please let me know. I hope this is helpful. Any questions? Feel free to ask in the comments.

Smart Image Resizing while Preserving Transparency With PHP and GD Library

Should be easy to find, right? All you want is a function that resizes an image to constraints (doesn't care if it should scale up or down), with possibility to select if you want to keep it proportional, and possibility to use either width or height as the constraint. Also, you want it to preserve transparency damn it! Surprisingly, I was unable to find a good function that does all that, so I decided to attempt writing it. This should do it.

Features/Usage:

  • If you pass width as 0 (zero) -- this function will disregard width, and use height as constraint. Same vice versa.
  • If you set "proportional" to false - the function will simply stretch (or shrink) the image to its full constraints.
  • If one of the dimensions is set to zero, and proportional set to "false" - then the image will be forced to stretch or shrink the other dimension, and disregard the zeroed dimension (leave it the same).
  • If proportional is set to true - the image will resize to constraints proportionally, once again, with possibility to have either width or height set to zero.
  • The function can use either linux "rm" command, or php @unlink. Most probably you don't need to ever use that flag, but on some setups - @unlink won't work due to user access restrictions.
  • The function will simply replace the file that you give it, with the resized file.
  • The function supports gif, png, and jpeg, and preserves the transparency of gif and png images.
  • Tested on GD version 2.0.28 only.

Essentially, everything happens just as logically expected. Please, if you see anything wrong, and you know ways to enhance, or optimize it, let me know.

Update: Thank you Joanne for finding a bug with proportional resize, and providing a great, more concise and clear solution to proportional resize. (see comments)

Update: The image transparency code was updated to a more thorough solution. See comments for info.

Update: There is a new parameter "output" which can be set to either

  • "file" - overwrite the given file (default)
  • "browser" - output image through http - with correct mime type
  • "return" - return GD Library Image object
  • or any filename of choice - save changed version to output path

Update: There is another new parameter "delete_original". Speaks for itself.

Please, excuse a few code misalignments. Bug with coloring filter.

<?php
function smart_resize_image( $file, $width = 0, $height = 0, $proportional = false, $output = 'file', $delete_original = true, $use_linux_commands = false )
    {
        if (
$height <= 0 && $width <= 0 ) {
            return
false;
        }

        $info = getimagesize($file);
       
$image = '';

        $final_width = 0;
       
$final_height = 0;
        list(
$width_old, $height_old) = $info;

        if ($proportional) {
            if (
$width == 0) $factor = $height/$height_old;
            elseif (
$height == 0) $factor = $width/$width_old;
            else
$factor = min ( $width / $width_old, $height / $height_old);  

            $final_width = round ($width_old * $factor);
           
$final_height = round ($height_old * $factor);

        }
        else {
            $final_width = ( $width <= 0 ) ? $width_old : $width;
           
$final_height = ( $height <= 0 ) ? $height_old : $height;
        }

        switch ( $info[2] ) {
            case
IMAGETYPE_GIF:
               
$image = imagecreatefromgif($file);
            break;
            case
IMAGETYPE_JPEG:
               
$image = imagecreatefromjpeg($file);
            break;
            case
IMAGETYPE_PNG:
               
$image = imagecreatefrompng($file);
            break;
            default:
                return
false;
        }
       
       
$image_resized = imagecreatetruecolor( $final_width, $final_height );
               
        if ( (
$info[2] == IMAGETYPE_GIF) || ($info[2] == IMAGETYPE_PNG) ) {
           
$trnprt_indx = imagecolortransparent($image);
  
           
// If we have a specific transparent color
           
if ($trnprt_indx >= 0) {
  
               
// Get the original image's transparent color's RGB values
               
$trnprt_color    = imagecolorsforindex($image, $trnprt_indx);
  
               
// Allocate the same color in the new image resource
               
$trnprt_indx    = imagecolorallocate($image_resized, $trnprt_color['red'], $trnprt_color['green'], $trnprt_color['blue']);
  
               
// Completely fill the background of the new image with allocated color.
               
imagefill($image_resized, 0, 0, $trnprt_indx);
  
               
// Set the background color for new image to transparent
               
imagecolortransparent($image_resized, $trnprt_indx);
  
           
            }
           
// Always make a transparent background color for PNGs that don't have one allocated already
           
elseif ($info[2] == IMAGETYPE_PNG) {
  
               
// Turn off transparency blending (temporarily)
               
imagealphablending($image_resized, false);
  
               
// Create a new transparent color for image
               
$color = imagecolorallocatealpha($image_resized, 0, 0, 0, 127);
  
               
// Completely fill the background of the new image with allocated color.
               
imagefill($image_resized, 0, 0, $color);
  
               
// Restore transparency blending
               
imagesavealpha($image_resized, true);
            }
        }

        imagecopyresampled($image_resized, $image, 0, 0, 0, 0, $final_width, $final_height, $width_old, $height_old);
   
        if (
$delete_original ) {
            if (
$use_linux_commands )
               
exec('rm '.$file);
            else
                @
unlink($file);
        }
       
        switch (
strtolower($output) ) {
            case
'browser':
               
$mime = image_type_to_mime_type($info[2]);
               
header("Content-type: $mime");
               
$output = NULL;
            break;
            case
'file':
               
$output = $file;
            break;
            case
'return':
                return
$image_resized;
            break;
            default:
            break;
        }

        switch ( $info[2] ) {
            case
IMAGETYPE_GIF:
               
imagegif($image_resized, $output);
            break;
            case
IMAGETYPE_JPEG:
               
imagejpeg($image_resized, $output);
            break;
            case
IMAGETYPE_PNG:
               
imagepng($image_resized, $output);
            break;
            default:
                return
false;
        }

        return true;
    }
?>

Improving Agility with PHP: Storing Multiple (Boolean) Values In a Single Integer

Say you have multiple checkboxes in your html page. At some point you will need to add a few more checkboxes for some configuration options. Or they could be a whole bunch of boolean flags in your program that you need to keep track of. Usually you would have to create numerous separate variables for each boolean flag, an array for all of them, or if they're stored in a database - you'd have to add more columns to store each "bit". Why bother altering the table structure (and doing any kind of special memory allocation) just for some puny bits? We can combine them all into a single integer, store them all in one place, and have ability to easily read/alter them, as well as add more boolean flags, or reassign the meanings of the old ones.

For example, we might have a list of songs on a page, and we might want to give a user an ability to check which songs are to be played and which to be omitted. Say, we have 7 songs. Each one can be included (1 - checked) or excluded (0 - unchecked). Let's represent it in a single boolean number. If all songs were unchecked, it would look like this: 0000000. If we decide to check the second, third, fifth, and seventh songs, it would look like this: 0110101. Each bit stands for a song. Now if we drop the leading zero from the resulting number - it would look like 110101. Quick check on a conversion calculator would tell us that in a decimal representation the number is simply... 53. We can see now that number 53 means that we have 2nd, 3rd, 5th, and 7th songs checked. Ok, this is understandable, but how to extract these values and work with them?

Simple. Each song has its own number. When 7th song is checked, it looks like this 0000001. When 6th song is checked, it looks like this: 0000010, etc. 10 converted to decimals is 2. 1 is 1. Just to remind you how the boolean system works - 100 is 4, 1000 is 8, etc. Let's assign these values in PHP define(), to read them easier.

<?php
define
( SONG1, 1 );   // 0000001
define ( SONG2, 2 );   // 0000010
define ( SONG3, 4 );   // 0000100
define ( SONG4, 8 );   // 0001000
define ( SONG5, 16 );  // 0010000
define ( SONG6, 32 );  // 0100000
define ( SONG7, 64 );  // 1000000
?>

Now, let's say user has selected the checkboxes like I mentioned above: 0110101. This number is actually 53. The first thing to discuss is - how to use PHP and the information that we have to extract each individual checkbox from this single number. There is really nothing to it!

Let's simply write:

<?php
$checkboxes
= 53// we could get this value from the database, or wherever we store our checkboxes

if ( $checkboxes & SONG1 ) { // this means song 1 is checked }
if ( $checkboxes & SONG4 ) { // this means song 4 is checked }
?>

That's it! The "$checkboxes & SONG4" is actually "53 & 8". It returns either "8" if the song is checked, or zero if it's not. In our case (0110101) the Song 4 is not checked - so we'll get a zero for it.

It's also very easy to change each individual flags - set or remove the checks from the songs. Say we want to check and remove the Song 4. Here's what we do:

<?php
// adding Song 4
if ( !($checkboxes & SONG4) ) { // always have to check if it's already flagged
  
$checkboxes += SONG4// flagging it
}

// removing Song 4
if ( $checkboxes & SONG4 ) {
  
$checkboxes -= SONG4;
}
?>

As simple as that.

Now it's obvious that if we need SONG8, SONG9, etc - we do not have to change the structure of anything - we can just define them as 128, 256, and keep on tracking them in that same integer.

There are many cases where this practice is beneficial. If you have tons of checkboxes, whose amount and meaning may vary, and you can't afford having so many columns in the database, and changing them constantly - this is the way to go. If you are afraid to mess with live-database too often - this way is better as well. This is simply a more agile and flexible approach to storing boolean values. Have fun!

Syndicate content