Monday, 10 March 2008

Rating content and calculating average ratings in MySource Matrix

A new feature was released in version 3.16.6 of MySource Matrix that allows you to calculate an average rating for content based on the comments and ratings users have made. The average rating is calculated as comments are created (or approved, it's up to you) and then stored in a metadata field. This field can then be used to sort pages based on their average rating.

Adding this feature to an existing comment system is fairly easy, but I'll just run through the basics of building a comment system first.

I've built a basic site with two standard pages. Both pages have a paint layout applied that nests in an asset builder and an asset listing that are used to create new comments and list existing ones. The general idea here is that every page will have a form that users can use to create new comments and apply a rating. Under the form will be a listing that prints the current comments and the ratings users applied to them.


The home page with a couple of comments and ratings

Asset Builder

The asset builder is configured to create comment assets. In my case, I have made them Live by default, but you could choose to create them as Under Construction so you can moderate comments. On the Create Locations screen, I have configured an Additional Create Location to create the comment asset under the current asset. I've made the links TYPE_2 so they are not shown in menus.


The bottom of the Create Locations screen, showing the additional create location


The create form for comments has been customised (bottom of the asset builder's Details screen) so that the rating can be added when the comment is created. I've also renamed Title to Subject.


The contents of the Comment Create Form Layout asset

Asset Listing

The asset listing is configured to list comment assets. The root node is set as the site asset, but this will be replaced dynamically by the current asset. Direct links only is set to Yes and there is a dynamic parameter set to replace the root node with the current asset's ID.

I've customised the No Results bodycopy to print a message encouraging users to post a comment and I've printed the rating as both a percentage and as a star rating in the default type format.


The contents of the Default Format bodycopy

Paint Layout

I've nested the asset builder and the asset listing in the default type format. Be sure to nest the asset builder first otherwise new comments will not appear until the next page refresh. This is obviously not a problem if you are moderating comments as they will not appear until they are approved.


The contents of the Default Format bodycopy

Adding Comments

Once the paint layout is applied, users are presented with the following form:


The home page before any comments have been added


Once they have added a few comments, we can start to work out the average rating. The image on the left shows two comment assets under the home page. Both are TYPE_2 linked so they don't appear in menus. By linking comments directly under each page, we can easily list them using a dynamic root node. If you are moderating comments, you may find it easier to create an asset listing of all Under Construction comments under the site with a link to their Simple Edit interface rather than manually looking for comments under pages.

Calculating Average Ratings

Now that we have our standard commenting system created, we can implement the new average rating functionality. All we do is apply a metadata schema to the assets that are rated and ensure there is a Text field in the schema in which to store the average rating. Then we create a trigger that sets the average rating when a new comment is added.

The metadata schema I have created is shown below.


This metadata schema contains a single section and Text field

Make sure you set a default value (e.g., zero) so that unrated content gets a default rating.

The trigger I have created is shown below. Remember to enable triggers on the Trigger Manager Details screen.


The trigger to calculate average ratings


Once this trigger is enabled, I added a new comment to the page to force the trigger to run. I now have three comments with three different ratings; 20%, 40% and 80%. Once the trigger has fired, the value 46 has been entered into the Rating metadata field for my page. Note that the rating has been rounded down so that an asset with a rating of 99.6% is not rated at 100%.


The metadata screen of the home page


Now I can configure an asset listing to show the most popular pages in my site. This asset listing is configured to list standard pages under my site and is sorted by the value of the Average metadata field. The Default type format is configured to print the asset's name and the average rating and I've limited it to 10 assets per page.


The asset listing of the most popular pages in the site


The rating is just a percentage, but you could get tricky with JavaScript and use the values to prints stars and other graphical elements. Just remember to put a textual value in there somewhere to ensure it is accessible.

And that's it. You can use this average rating anywhere you normally use metadata, including sorting search results. And of course, this is MySource Matrix so you can configure all the frontend interfaces shown here in any way you want.

Using PHP_CodeSniffer in an SVN pre-commit hook

I've just commit a new script to PHP_CodeSniffer called phpcs-svn-pre-commit. It sits in the scripts dir with phpcs and phpcs.bat. This script was contributed by Jake Bates, who has also volunteered to maintain the Debian package, and will be available in the 1.1.0 release.

Using the script is pretty easy, but you'll need to modify it slightly.

Edit /path/to/PHP_CodeSniffer/scripts/phpcs-svn-pre-commit and replace @php_bin@ in the first line with the path to the PHP CLI. For example,

#!@php_bin@
becomes
#!/usr/bin/php

Then, ensure the path to svnlook is correct by modifying the line:
define('PHP_CODESNIFFER_SVNLOOK', '/usr/bin/svnlook');

Now, add the following line to your pre-commit file in the SVN hooks directory:
/.../phpcs-svn-pre-commit "$REPOS" -t "$TXN" >&2 || exit 1

This will cause the SVN commit to fail if PHP_CodeSniffer finds any errors. The error report will be displayed to the user so they can fix errors before attempting the commit again.

You can also use all the standard phpcs command line options to do things like set the standard to use, the tab width and the error report format:
/.../phpcs-svn-pre-commit --standard=Squiz --tab-width=4 ...

And some example output:
$ svn commit -m "Test" temp.php
Sending temp.php
Transmitting file data .svn: Commit failed (details follow):
svn: 'pre-commit' hook failed with error output:

FILE: temp.php
---------------------------------------------------------------
FOUND 1 ERROR(S) AND 0 WARNING(S) AFFECTING 1 LINE(S)
---------------------------------------------------------------
2 | ERROR | Missing file doc comment
---------------------------------------------------------------

Friday, 7 March 2008

Creating an SVN repostiory on OS X

I've been playing around with an SVN pre-commit hook for PHP_CodeSniffer that was submitted by Jack Bates. Part of that process required me to create a SVN repository for testing. While it was fairly easy overall, there were a few error messages that I got stuck on.

Firstly, creating the repository was dead simple:
$ create --fs-type fsfs /Users/Greg/TestSvnRepo

But then I couldn't checkout my repository. I'd get errors like:
$ svn co file:///Users/Greg/TestSvnRepo
Unable to open an ra_local session to URL
...
Expected version '3' of repository; found version '5'

Common suggestions on mailing lists point to svn not being compiled with ra_local support, which was not the problem in my case:
$ svn --version
svn, version 1.1.4 (r13838) compiled Jul 23 2006, 14:53:55
...
The following repository access (RA) modules are available:
* ra_dav : Module for accessing a repository via WebDAV (DeltaV) protocol.
- handles 'http' schema
- handles 'https' schema
* ra_local : Module for accessing a repository on local disk.
- handles 'file' schema
* ra_svn : Module for accessing a repository using the svn network protocol.
- handles 'svn' schema

I did find one useful article though. It mentions that different versions of svn software can cause problems. I believe this is more common when moving repositories between versions, or when connecting to a remote server, but in my case the problem was my version of svnadmin:
$ svnadmin --version
svnadmin, version 1.4.4 (r25188) compiled Sep 23 2007, 22:32:34

So my svnadmin utility is version 1.4.4 but the svn utility itself is 1.1.4. Adding the pre-1.4 command line argument suggested in the article worked a treat:
$ svnadmin create --fs-type fsfs --pre-1.4-compatible /Users/Greg/TestSvnRepo
$ svn co file:///Users/Greg/TestSvnRepo

Thursday, 14 February 2008

PHP_CodeSniffer == JS_CodeSniffer?

There has been one item on my PHP_CodeSniffer todo list for a long time; to get PHP_CodeSniffer to enforce coding standards for JavaScript files. I've finally found the time to get going with this project and I've made good progress over the last couple of days.

My local copy of PHP_CodeSniffer now uses the file extension to determine which tokenizer to use to parse each file. I've moved the tokenizing code out of PHP_CodeSniffer_File into a PHP tokenizer and have added a new JS tokenizer. Once the file is tokenized, all existing sniffs can be run on it, but obviously most of the existing ones are only going to work correctly for PHP files.

The next step is to look at all the existing sniffs and identify those that are PHP only and those that would also work for JS files. Once that happens, each will be flagged using a protected member var so developers of coding standards know which tokenizers a sniff supports. I'll only be doing this for the Squiz and Generic standards to start with as I'm sure PEAR has no desire to start writing a JavaScript coding standard.

For developers who have written their own sniffs, you don't need to change anything to keep your existing sniffs and standards working. You only need to make changes if you want to start checking JavaScript files as well.

My overall goal is to get PHP_CodeSniffer to check all file types that PHP web developers would commonly use, including JavaScript, CSS, HTML and XML. It's going to take a while to get there, but JavaScript files are easily the most complex files to parse in that list, so I'm well on the way. I'm hoping to get this change into CVS sometime next week and get a release candidate out after I've written a decent number of JS sniffs.

Tuesday, 12 February 2008

When PHP string comparions go wrong

I'm a little ashamed to admit that I didn't know about this before, but I've just never come across it. When PHP compares two strings it converts them to integers if they both appear to be numbers, unless you use the === operator to compare types as well. The comparison operator docs state:

If you compare an integer with a string, the string is converted to a number. If you compare two numerical strings, they are compared as integers. These rules also apply to the switch statement.

Keeping that in mind, take a look at this code:
if ('000E00080001' == '000E0008') {
echo 'equal';
}
This statement evaluates to TRUE, but I wasn't sure why.

Even reading the comparison operator docs doesn't make it clear, but it does link off to a very important piece of information about strings. In particular, the section on string conversion to numbers states:
The string will evaluate as a float if it contains any of the characters '.', 'e', or 'E'. Otherwise, it will evaluate as an integer.

And the truth shall set you free!

The letter "e" in my two strings is considered part of the exponent of a number. In both cases, the number is "0" followed by an exponent. When PHP compares those two strings, it compares them as "zero to the power of ..." because they are both considered numbers. So in this case, it is comparing zero with zero and finding it is TRUE.

This also works with decimal points. This evaluates to TRUE as well:
if ('0000.' == '0000.0') {
echo 'equal';
}
And just to confirm what is going on, adding a character that is not considered part of a number results in the expected behavior. This evaluates to FALSE because PHP no longer considers these values numbers:
if ('000E000F0001' == '000E000F') {
echo 'equal';
}
And, if the initial portion of our number is not zero, the exponent modifies the value. We are no longer comparing "zero to the power of ..." (which is always zero) in the following example; we are comparing "one to the power of ...", so it evaluates to FASLE:
if ('001E00080001' == '001E0008') {
echo 'equal';
}
So what is the solution? You can either use strcmp() to compare your strings, or use the === operator to compare types as well. When PHP compares types, it will not try and convert strings to numbers.

Notch one up for using the === equal operator, which we use exclusively in MySource4. We even go as far as banning type-insensitive and implicit comparisons using PHP_CodeSniffer. If you want to do the same, you can get PHP_CodeSniffer from PEAR and use the included Squiz standard.

Monday, 4 February 2008

PHP_CodeSniffer 1.0.1 released

I've just uploaded PHP_CodeSniffer version 1.0.1, which contains 6 bug fixes and adds some new sniffs into the Squiz and MySource standards after some lobbying by the MySource4 team. The coding standard we use for MySource4 now has 94 different sniffs, each containing up to 5 different checks, so it is getting fairly strict. The code should be pretty enough to frame and stick on your wall!

One important change has been made to the PEAR standard in this release. A recent RFC that asked PEAR developers to vote on forcing protected member vars to be prefixed with an underscore got me looking for the relevant sniff in PHP_CodeSniffer. (I didn't see the call for votes, but I would have voted not to prefix protected vars with an underscore.) While PEAR decided against adding this new standard, I did realise that PHP_CodeSniffer was not currently enforcing the existing standard. The 1.0.1 release adds a new sniff to the PEAR standard to ensure only private members vars are prefixed with an underscore.

You can view the full changelog, and download the release, on the package download page.

Wednesday, 23 January 2008

Aleks Bochniak writes... How to make a tag cloud (using Matrix)

Apart from being better at Matrix implementation than me by building his blog using MySource Matrix, Aleks has also written a great tutorial on how to build a tag cloud using Matrix. He uses metadata for tagging, an asset listing to show the tags and some JavaScript to make the magic happen.

Well worth a read if you're looking to implement a tag cloud on your own site, or if you just want to see an example of different parts of Matrix coming together to produce something beyond what they were designed to do.