Using Zend Search Lucene in a symfony app

[tags]zend, search, lucene, zend search lucene, zsl, symfony,php[/tags]

Read full post
Digg-style AJAX comment editing in PHP/symfony

Digg“-style anything can be pretty slick. The AJAX-interactions on that site make it very fun to use. It’s styles have been copied everywhere, and are definitely worth copying. The latest feature that had caught my eye was the ability to edit your comments for a set time after posting them. Of course, it wasn’t just the ability to edit comments, it was AJAX too and it has a timer.

This is totally something I could use on a restaurant review site. So I started on this project. It’s pretty straight forward. For all of your posted comments you check if the owner of them is viewing them within 3 minutes of posting the commen. 3 minutes is usually enough time to notice you made a typo, but if you disagree I’ll leave it to you to figure out how to adjust the code.

For example, I make a comment, realize I spelled something wrong and then I can click on my comment to edit it. Of course using AJAX means this all happens without having to reload the web page. Therefore the edits are seemingly quick. So let’s add it to any web site.

In Place Forms

First and foremost, the ability to edit a comment means you have a form that you can use to edit and submit your changes. But rather than deal with creating a boring unAJAXy form, we’ll enlist the help of script.aculo.us.

First, each comment is rendered using the following HTML and PHP:

<div class="review_block" id="comment_<?php echo $comment->getId() ?>">  
	<p class="author"><?php echo link_to_user($comment->getUser()) ?> - <?php echo $comment->getCreatedAt('%d %B %Y') ?></p>
	<div class="review_text" id="review_text_<?php echo $comment->getId()?>"><?php echo $comment->getHtmlNote() ?></div>
</div>

Note that this div and it’s child div have unique ids that we can refer back to (comment_n and review_text_n where n is the id of the comment). We can use this to interact with the DOM via JavaScript. What we do is for each comment, we check if it is owned by the current visitor and if it’s within our prescribed 3 minute window. We can do that with some simple PHP:

<?php if ($comment->getUser() && $comment->getUserId() == $sf_user->getId() && time() < 181 + $comment->getCreatedAt(null) ): ?>
	<script type="text/javascript">
	//<![CDATA[
		makeEditable('<?php echo $comment->getId() ?>', "<?php echo url_for($module . '/save?id=' . $comment->getId()) ?>", "<?php echo url_for('restaurantnote/show?id=' . $comment->getId() . '&mode=raw') ?>", <?php echo 181-(time() - $comment->getCreatedAt(null)) ?>);
	//]]></script>
<?php endif ?>	

As you can see we run the makeEditable() function for each applicable comment. As you can guess, makeEditable() makes a comment editable. For parameters it takes the comment’s id (so it can refer to it in the DOM and other background scripts). It also takes as an argument the “save” URL as well as a URL from which it can load the raw comment. The last argument is for the timer.

Here is our function:

var editor;
var pe;
makeEditable = function(id, url, textUrl, time) {
	var div = $("review_text_" + id);
	
	pe = new PeriodicalExecuter(function() { updateTime(id); }, 1);
	
	Element.addClassName($('comment_' + id), 'editable');
	new Insertion.Bottom(div, '<div class="edit_control" id="edit_control_'+id+'">Edit Comment (<span id="time_'+id+'">'+time+' seconds</span>)</div>');
	
	editor = new Ajax.InPlaceEditor(div, url, { externalControl: 'edit_control_'+id, rows:6, okText: 'Save', cancelText: 'Cancel', 
	loadTextURL: textUrl, onComplete: function() { makeUneditable(id) } });
}

It does a couple things. It runs a PeriodicalExecuter to run the updateTime function which updates our countdown timer. It adds a CSS class to our comment div. It adds a control button to edit a comment. Lastly it uses the script.aculo.us Ajax.InPlaceEditor to do most of the magic. The hard part is done.

Periodic Execution Timer

So the updateTime function is reasonably simple. It finds the time written out in the DOM and decrements it by 1 second each second. Once it hits zero seconds it disables itself and the ability to edit the block. Let’s take a look:

updateTime = function(id) {
  var div = $("time_"+id);
  if (div) {
    var time =  parseInt(div.innerHTML) - 1;
    div.innerHTML = time;
  }
  if (time < 1) {
    pe.stop();
    var editLink = $('edit_control_'+id);
    if (Element.visible(editLink)) {
      makeUneditable(id);
      editLink.parentNode.removeChild(editLink);
    }
  }
}

Call backs

We’ll need a few call backs for the editor to work properly. Since many content pieces are converted from something else to HTML and not directly written in HTML we’ll need a callback that will load our text. We’ll also need a callback which will save our text (and then display it).

Load Text

The first call back we can see is referenced in the makeEditable() function. In our example it’s:

url_for('restaurantnote/show?id=' . $comment->getId() . '&mode=raw');

Which is a symfony route to the restaurantnote module and the show action with an argument mode=raw. Let’s take a look at this action:

public function executeShow ()
{
	$this->restaurant_note = RestaurantNotePeer::retrieveByPk($this->getRequestParameter('id'));
	$this->forward404Unless($this->restaurant_note instanceof RestaurantNote);
}

All this does is load the text (in our case the [markdown] formatting) into a template.

Save Text

The save text url in our example is:

url_for('restaurantnote/save?id=' . $comment->getId());

Using the Ajax.InPlaceEditor the value of the text-area is saved to the value POST variable. We consume it in our action like so:

public function executeSave() 
{
	$note = RestaurantNotePeer::retrieveByPk($this->getRequestParameter('id'));
	$this->forward404Unless($note instanceof RestaurantNote);
	if ($note->getUserId() == $this->getUser()->getId()) {
		$note->setNote($this->getRequestParameter('value'));
		$note->save();
	}
	$this->note = $note;
}

The note is also sent to a template that renders it, so when the save takes place, the edit form will be replaced with the new text.

Conclusion

As you can see with some script.aculo.us and symfony, it’s fairly easy to mimic “Digg-style” in-place comment editing. You can test out a real example by visiting reviewsby.us.

Read full post
Biking Shoes

Shimano SH-MT20

I think I now have more shoes than my wife. I just bought some Shimano’s biking shoes, bringing my total pairs of shoes up to 7:

  1. Nunn Bush Dress Shoes: These are the shoes my brother suggested I get as dress shoes. They are dressy, but they are also comfortable for walking. I’ve had them forever, and wear them the least.
  2. Black shoes with some red: These were some laceless shoes that I used to wear regularly. Now they are dirty and I use them for working around the house.
  3. Semi-casual black shoes with zippers: These shoes are/were my daily work shoes. They aren’t dress shoes, but they are black so they work for going to the office.
  4. AVIA tennis/running shoes: These are an inexpensive pair of shoes I bought last year, when I wanted something sportier than my zipper shoes. They’ve been real nice for biking (using toe-clips), and I liked wearing them around the office.
  5. Brown sandals from India: These are the sandals I wore at my engagement ceremony and wedding and now they are my lounging shoes.
  6. Even dressier shoes: These were another cheap pair from India (actually the sandals were somewhat expensive). I got them because they were cheap. I plan on leaving these at my office.
  7. My Shimano SH-MT20 shoes: I took the plunge and bought some Shimano shoes and clip-less pedals.

No, I’m not a shoe-junkie. I really just need the pair of shoes that I can use for working around the house, the biking shoes, and the dress shoes. But now I have a whole bucket of choices for a while. I might store some of them for a while.

Read full post
How Object-Relational Mapping saves time and makes your code sexy

Object Relational mapping is a way of transparently interacting with a relational database by using objects. Each database table is a new class and each row in the table is a single object. Relations between tables are now relations between classes.

It wasn’t until I started using symfony and propel that I started appreciating ORM. I started working on significant projects and the time it would take me to do things went down quite a bit. Prior to Propel, I had a lot of library files that would store and retrieve information for me.

class lib {
	function valid_user($username, $pw)
	{
		$q = "SELECT id FROM user WHERE username LIKE '$username' AND password LIKE '$pw'";
		return DB::do_query_select_one($q);
	}
}

Not too bad, but a lot is buried in my hypothetical do_query_select_one function. Let’s compare this to the ORM (propel) version:

class myTools {
	public static valid_user($username, $pw)
	{
		$c = new Criteria();
		$c->add(UserPeer::USERNAME, $username);
		$c->add(UserPeer::PASSWORD, $pw);
		return UserPeer::doSelectOne($c);
	}
}

That’s a lot of extra writing, and as someone who’s quite proficient in SQL, you can see why I initially laughed it off. Let’s take it a step further. Sure we have twice as many lines of code, but what would the calling functions do after they check to see a user is valid or not?

In our non-ORM world we would attempt to iterate through each row. find some corresponding ACL table and add all these elements to a session variable. This can get old fast. Let’s see how that would look:

if ($user = valid_user($_POST['username'], $_POST['pw'])) {
	// $user we populated from our made-up 
	// DB::do_query_select_one function.  Let's pretend that's easy.
	$id = $user['id'];
	$sql = 'SELECT group FROM acl WHERE user_id = ?';
	$ps = prepare_statement($sql, $id);
	// ...
 	}

That’s neat, but in the ORM world we do it like this:

if($user = valid_user($_POST['username'], $_POST['pw']))
{
	$user->getACLs();
}

All the extra database calls are safely encapsulated in our class. No worries. It’s just a one-liner.

Putting things into functions

Another neat trick is putting some redundant code into simple functions. By using a criteria object, you can cleanly create some functions that take an input criteria and return one with specific parameters:

function securify(Criteria $c)
{
	// makes sure the user is still valid
	$c->add(User::EXPIRES, time(), CRITERIA::GREATER_EQUAL);
	$c->add(User::VALID, true);
}

Now all we need to do every time we call a user is call securify on the Criteria object to make sure we have a valid user that hasn’t expired.

Deleting objects

Getting rid of data: $user->delete().

Customizations are saved

Let’s say you want the User object to have some customizations. Any of those customizations will persist even after you change the model, since User inherits from a BaseUser class which is dynamically generated from a defined schema. This can save a ton of time when your model changes. Instead of finding every instance of a call to see if a user is logged in, you can change your custom classes and not have to worry. If this had been the case for me, I’d have saved myself and my client a few hours of coding.

Conclusion

ORM relegates the database to simply being a store for persistent objects. What this means is you no longer need to rely on half-baked SQL queries to save and load objects. You can let the objects take care of that themselves, without worrying about the database back-end. This allows you, the programmer, to do your job of manipulating objects to execute the goal of a web site. Enjoy.

Read full post
How to %^&* yourself over with giant to-do lists

Q: How do you eat an elephant?
A: One bite at a time.

There’s a problem with that adage. Your body can only eat so much over a given time. The elephant will probably spoil before you make a significant dent in it.

This is no different than someone who tries to tackle an impossible to-do list. Imagine, week after week, eating and eating at this elephant. You’re digesting quite a bit, but there’s so much left. You might just be inclined to give up and quit. You’ll feel like you accomplished nothing (of value), even though you really did quite a lot of work.

No deadlines

The biggest mistake you can have with a to-do list is omitting a “due date” or deadline. If you’re to-do list looks like…:

* Write Novel
* Shovel Snow
* Cut up elephant
* Train for triathlon

…then there’s no concept of when these items need to get done. That means all these items are in the forefront at once.. or could be. A good start or end date will properly prioritize these:

* Shovel Snow - 11/1
* Cut up elephant - 11/23
* Write Novel - 12/1-3/1
* Train for triathlon - 4/1

So now, the novel is going to be written in winter. You’re going to shovel snow on the first day of November. You’ll cut up the elephant just before Thanksgiving. And you don’t need to train for the triathlon until early spring.

The deadlines serve multiple purposes. The first is, you know roughly when to do what. You shovel snow next week. You write the novel in winter. You train in spring. The other important purpose is you know when to stop. A “deadline” means that line item or project or whatever has a “drop dead” date. That means you don’t need to work on it any more. For example, if you don’t finish that novel, you don’t need to let it linger. Of course, if an item is really important and salvageable you can re assign a due date.

Too many items at once

Another related setup for disaster is having too many items at once. If you go to a buffet with the intent of eating the proprietor out of house and home, the best bet isn’t to shove everything you intend to eat all at once on a plate. No, of course it’ll look comical and you’ll never finish. At least, you won’t finish it before a lot of the food gets cold. Go up multiple times.

If you’re master to-do list is too large, it can be overwhelming. Even if you get done with what’s due now, just looking at items in the future will not only distract you, but take away from any sense of accomplishment you might get. It might help to just start your day with a small to-do list written on a piece of paper, or maybe in another file. Just look at that list until you finish. You don’t need to think or plan for the future except at the very beginning of your day, and maybe at the end of the day once you’ve finished today’s items.

Too detailed

The last disaster area is details. Too many details is when it takes more time to write down what you’re going to do than to just do it. It’s at this point where you’re writing things down just to cross them off… and as satisfying as it might be at the time, over the long run you’re not accomplishing very much.

My somewhat working system

Currently my working system takes a lot of this into account. I have one master todo list which I look at daily. I only focus on the top elements that are due in the near term, unless I have nothing due soon. Once I read that, I make a small list of things to do for the day or next block of hours. Once I’m through those (or once it’s a new day) I start over.

The whole point of a to-do list is to have things that you’ll do. Don’t make a large list if it’s undoable. It’ll wear you down and prevent you from doing the few things you can do. Start small and you can go far.

Read full post
New Breezer

I picked up my new bike Wednesday night from Jim at Hiawatha Cyclery courtesy of my friend Jason dropping me off at the shop. Well, I’m glad I have a commuter worthy bike. Between the time my bike got stolen and today, I had 100 excuses not to ride, 100 reasons to be somewhere, and a 100 bikes on hand to use or borrow. What I really wanted was my Trek, and I was hurting pretty bad. I think this Breezer will fill the role nicely.

Most people I know, don’t bike as much as I do - or rather, don’t bike as their primary means of transportation. Here’s what I had at my disposal. Included were reasons I didn’t want to ride them:

  • Huffy Mt. Bike: This was set for winter mode, and I really didn’t want to ride a mountain bike around town in the summer… not one that’s so heavy.
  • Huffy Beach Cruiser: Had to install a bike rack so it was commuter worthy. Can’t really bike to work without a rack.
  • Katie’s Bike: I rode it once into work. It’s setup properly, but the dimensions aren’t for me.
  • Coworker’s Specialized Road Bike: Felt too expensive… I’m not used to road-bikes. Not a commuting bike, etc, etc.
  • Cannondale F400: This is a pretty sweet, light mountain bike that my brother found. It’s small, it’s a woman’s bike, it’s not set for commuting and needed a lot of work. But it was an interesting find. Might turn that into a hybrid for Katie if I can figure it out.

I’m sure there were more offers, but most people don’t have commuting bikes. They either have mountain bikes, or road bikes, but nothing well suited for commuting, even though a lot of bikes get repurposed for just that. So it’s no wonder why I’m the first person to pick up the nerdy looking Breezer from Hiawatha Cyclery.

Sure, it’s the nerdy bike. It’s really good at what it does, but it doesn’t look fast or zippy, but it looks smart. It’s bright green (not Midnight Blue like I had wanted, but not bad either), has all the bells and whistles to dorken it up: rack, fenders, bell, generator-lights. It’s a 3-speed internal shifter. And it’s begging for some new toys (since all my old ones were stolen with my bike).

Since Jason dropped me off, I had 7 miles to cruise through town through Minnehaha Parkway. I was dressed up slightly, but no need to worry, this bike had a chain guard. Jim gave me directions to the Parkway as well as a complimentary water bottle and cage to keep me hydrated (or at least give me the option of hydrating myself). It’s an excellent place to try out a new bike with plenty of hills and curvy roads.

My first adjustment which was immediate was the 3-speed internal shifter. I immediately felt like I could graduate to 7-speeds if I had the money, but at the same time, there’s a comfort in having only three choices: low going up hill; medium on flat surfaces; and high going down hill. Took some getting used to the grip shifter (I kept wanting the gears to shift backwards). The toughest part with three speeds is some hills could have used a lower gear, the higher gear could have been higher, but I made do. I think I’ll be fine with this though. Especially for in town errands. I’ll also have to get used to a diamond frame. My Trek had a downward sloping top-tube that made stoplights pretty easy.

I need to get used to the bell, it’s something I know I should have, but never got. Now I have it, so I can finally use it. I hate having to alert people to my presence. I really like the ring lock that locks the back wheel. So many days do I pull my bike out, and realize, crap, I need to get something from the house before I leave. Now I can easily free lock it.

As for the toys, I’m really excited that Performance Bike is having it’s double points weekend. I plan to score a grocery pannier, some tool packs, tools, and maybe a bike computer for new ride. I still have a gift certificate for the store too, so I’m in luck.

Before I start going on adventures, time to record the serial number and keep it safe (and probably leave the numbers with Jim).

Read full post
Slick Designs and Bad Usability: Translucent Text

Minneapolis Central Library

Not to pick on the Minneapolis Public Library again… but they are really good at featuring usability flaws. This one is also related to doors, and can very easily be related to web and software usability (if you can’t tell, web and software usability are the exact same things as the usability of everyday things).

Unfortunately I didn’t have my digital camera, but the Emergency Exit doors at the Minneapolis Public Library feature a translucent stenciling that says, “Emergency Exit” or “Emergency Exit Only.” Of course, it was very easy to gloss over this important note attached to the door, after all, the door looked fairly ordinary, and opening it would seemingly bridge access from one area of the building to another, therefore inviting one and all to open it. The cautionary notice about it being for Emergencies is easily dismissible.

As a matter of fact, you know the nice translucent sign was failing to notify people when a laser-printed notice was taped to each and every emergency exit as a label.

Obviously the designers were going for aesthetic. “Sure, we need an Emergency Exit, but it doesn’t have to stick out.” In the real world, it seems people preferred having no aesthetics (laser printed sheets of paper are temporary, right) and usability over aesthetics and fire-alarms.

The closest thing I can think of in the electronic world is the “close” feature of an application or a pop-up window. If it says close, you know what it means. If it’s an “X” you know what it means. If it’s in “red” and “red” isn’t part of the design, you know to maybe read what you’re doing first. But if we decide to have the close button look really pretty and part of that design means not labeling it or coloring it different from … oh let’s say the “maximize window” button. It’s easy to see how much user-frustration you can cause.

I really wish I was there to see just how many times that Emergency Door was pushed before some library staff decided to whip open Microsoft Word and type in the largest font possible “Emergency Exit Only” and Scotch tape it on that door.

Read full post
Untuitive Interfaces: A look at MPL's Automatic Doors

Minneapolis Central Library

Every now and again, there’s a tried and tested way of doing something and it gets enhanced somehow. Sometimes these enhancements are pure genius and sometimes they are half-baked. Today, we’ll look at the half-baked automated doors at the new Minneapols Public Library.

Automated doors are all fairly standard. There’s either a platform that opens when weight is applied (a la grocery stores), or there are large buttons with an engraved handycapped** symbol placed at wheelchair height. Usually automated doors only work well when they are moved by the motor, and give quite a bit of resistance to manual operation.

These aren’t the most perfect designs in the world, in fact, there’s plenty of room for change, for better or worse. MPL’s design involves making the entire door the button. You push the door slightly, and then it opens (and somehow knows which direction to open, as to not smack you in the face). Pure Genius… almost. While logically this makes sense, the flaw is in the execution. When I approached the door, I pushed it, like it beckoned, and then pushed it again… and again, and then eventually it started opening. Since it’s motorized, you can’t easily push it manually. This 5-second frustration was more than just me, it was everyone wanting to get in.

Fortunately it’s not a bad design, it’s just unresponsive. If I pushed it, and the actuator took over immediately in a single smooth motion, this would be perfect. Wheel chairs would gently run into the door and the door would open up. People would attempt to push open the doors and “voila!” it’d give you a helping hand.

This really isn’t so much a rant about automated doors, so much as its a look at what I’d call as “untuitiveness.” You have something which people do intuitively (e.g. push a door open) and then you break it. In our example pushing a door to open it is now broken, because although pushing the door slightly triggers the actuator, this is a lot different than what people expect. Plus the added wait time results in more confusion.

This can apply to web sites. For example, let’s take scroll bars. A web site that doesn’t fit in a browser window will often have horizontal and/or vertical scroll bars so that you can view the entire page. We can easily change the interface for a scroll bar. Maybe it’s a thumbnail in the corner that shows a view finder (a la Photoshop ) and we use that to navigate and disable the scroll bars. It’s arguably a better interface, but it’s going to cause problems for the end user ultimately since they 1. aren’t used to it, and 2. there might be some lag-time (assuming it’s implemented with Flash or DHTML).

This isn’t to say we shouldn’t come up with creative new ways of doing things – we should – but, some care needs to be taken toward usability. After all, your site is just one site on the Internet, even if you have a high ranking web site, you’re risking huge user-frustration if you try to do things too different – to “untuitive.”

Read full post
Usability for Nerds: Traversing URLs

A recent peeve of mine is URLs that you can’t manually traverse. Let me explain. Let’s say you visit http://reviewsby.us/restaurant/cheesecake-factory. You should manually be able to remove cheesecake-factory and see an index page of restaurants at http://reviewsby.us/restaurant/. It makes logical sense for that page to be something that would enable you to find more restaurants.

This is a throwback to static web sites, that consisted of directories and files. If you accessed a file by its name, you would see the contents of the file (possibly filtered by the server). If you accessed a directory, you would see an index of files. In the world of web apps, however, URLs are made up.
Web apps where these gaps are missing can be especially frustrating when you use your browser’s history. Often I reference the symfony api. The URL is http://www.symfony-project.com/api/symfony.html. All the other URLs in the API listing begin with http://www.symfony-project.com/api/, so you could assume that http://www.symfony-project.com/api/ is an index page.

URL autocomplete

It’s not the index page (http://symfony-project.com/api/symfony.html is). If you googled for sfConfig and got to http://symfony-project.com/api/symfony/config/sfConfig.html and didn’t feel like figuring out the navigation structure… or let’s say at a later date you’re using your browser’s URL auto-complete feature, you will get a 404 Error1.

Web applications can try to mirror directory indexes with pretty URLs, but often have a few gaps as every URL (or level of URL) needs to be designated in the app. Its a good idea to fill those in as it is another way to navigate a web site.


  1. Not to pick on the wonderful symfony development team, but I truly do see this a lot on their site. I'm sure they'll setup a redirect or something to fix this. Or probably call me out on the fact that many of my sites violate this principle.

Read full post