Just noticed there have been some changes on the PEAR site. Check it out - no nav menu on the left, spacier for the main content…
Archive for the 'PEAR' Category
PEAR site minor redesign
Sunday, January 6th, 2008Text_Highlighter 0.7.1 and hiliteme.com updates
Tuesday, January 1st, 2008In today's busy schedules there's less and less time to give love to our favorite open source projects. Luckily though, there's January 1st when you're supposed to relax (or suffer the consequences of partying on Dec 31st). Anyway, today I found the time to fix two bugs in Text_Highlighter and also include the patch from Daniel Fruzynski that adds support for highlighting VBScript. Wo-hoo!
Also updated hiliteme.com so you can try the package or simply highlight some code before posting it to your blog.
Luckily for me, the PEAR site uses GMT timestamps, so the release date of Text_Highlighter 0.7.1. is Jan 2nd. This way I hope it doesn't look like I need a life that badly.
Image_Text 0.6 beta is out
Thursday, April 19th, 2007This is my first PEAR release and I was actually surprised how easy it is to package and roll out a release.
So you have your local copy of the CVS repository that contains the scripts you want to release as part of the package. In order to release, you need package.xml, a configuration file, which you can either create yourself or have a script (which uses PEAR_PackageFileManager) to create the xml file for you.
The pear command line tool does all the rest.
pear convert- creates package2.xml based on your package.xml. (package2 is the newer improved version of package2.xml. You can actually use PEAR_PackageFileManager2 instead and skip this step)pear package- creates the package archive which then you upload to pear.php.netpear cvstag package2.xml- tags the cvs repository with a tag figured out from the package2. In my case the tag was RELEASE_0_6_0beta
Thanks!
As stated in the change log notes, many thanks go to Christian Weiske and James Pic for helping out with this release!
Performance tunning with PEAR::DB
Tuesday, January 16th, 2007If you use PEAR::MDB2, you can set a custom debug handler and collect all the queries you execute for debugging and performance tunning purposes, as shown before. But what if you're using PEAR::DB? Well, since PEAR::DB doesn't allow you such a functionality out of the box, you can hack it a bit to get similar results.
Simple app
Let's say you have a simple app:
require_once 'DB.php'; $dsn = 'mysql://root@localhost/test'; $db =& DB::connect($dsn); $db->setFetchMode(DB_FETCHMODE_ASSOC); $sql = 'SELECT * FROM zipcodes'; $result = $db->getAll($sql); $result = $db->getOne($sql); $result = $db->getCol($sql); $result = $db->getAll($sql); $sql = 'SELECT zipcode FROM zipcodes'; $result = $db->getAll($sql); $result = $db->getAll($sql); $sql = 'SELECT CONCAT(zipcode, " - ", city) FROM zipcodes'; $result = $db->getAll($sql);
Of course, this is an oversimplified example, usually you have more included files, class libraries and such, and it's not difficult to lose track of the database work as your app grows in complexity and size.
Now let's debug this app to see what type of database work it does.
Hacking PEAR::DB
In my case, I'm using MySQL, so I need to find the DB/mysql.php file in my PEAR directory. I open that file and find the simpleQuery() method. That's where all queries and up, sooner or later. I find this piece of code:
if (!$this->options['result_buffering']) { $result = @mysql_unbuffered_query($query, $this->connection); } else { $result = @mysql_query($query, $this->connection); }
Then I hack this piece of code, adidng some lines before and after it. The result:
// start $start_time = array_sum(explode(' ',microtime())); // end if (!$this->options['result_buffering']) { $result = @mysql_unbuffered_query($query, $this->connection); } else { $result = @mysql_query($query, $this->connection); } // start $query_took = array_sum(explode(' ',microtime())) - $start_time; @$GLOBALS['global_query_counter']++; @$GLOBALS['all_the_queries'][$GLOBALS['global_query_counter'] . ' - ' . $query] = $query_took; //end
Now as my app's pages are executed, I'll collect invaluable DB information.
Reporting
Let's see what we've collected.
You can add different types of reports in the footer of your application, or better yet, you can register a shutdown function to do the same. Here are some reporting ideas:
// report 1. echo "<pre>All the queries, by the order they are executed:\n"; print_r($GLOBALS['all_the_queries']); echo '</pre>'; // report 2. echo "<pre>All the queries, ordered by the time they took, descending:\n"; arsort($GLOBALS['all_the_queries']); print_r($GLOBALS['all_the_queries']); echo '</pre>'; // report 3. $sum = 0; foreach ($GLOBALS['all_the_queries'] AS $t) { $sum += $t; } echo '<pre>'; echo 'Total number of queries: ' . $GLOBALS['global_query_counter'] . "\n"; echo 'Total time spend querying: ' . $sum; echo '</pre>'; // report 4. $distinct = array(); foreach ($GLOBALS['all_the_queries'] AS $q=>$t) { $parts = explode(' - ', $q); unset($parts[0]); $query = implode(' - ', $parts); @$distinct[$query]++; } echo "<pre>How many duplications:\n"; arsort($distinct); print_r($distinct); echo '</pre>';
Report results
Let's see what these reports gives us.
All the queries, by the order they are executed:
Array
(
[1 - SELECT * FROM zipcodes] => 0.00626707077026
[2 - SELECT * FROM zipcodes] => 0.00730204582214
[3 - SELECT * FROM zipcodes] => 0.00796985626221
[4 - SELECT * FROM zipcodes] => 0.00654602050781
[5 - SELECT zipcode FROM zipcodes] => 0.0058650970459
[6 - SELECT zipcode FROM zipcodes] => 0.0239379405975
[7 - SELECT CONCAT(zipcode, " - ", city) FROM zipcodes] => 0.00581502914429
)
All the queries, ordered by the time they took, descending:
Array
(
[6 - SELECT zipcode FROM zipcodes] => 0.0239379405975
[3 - SELECT * FROM zipcodes] => 0.00796985626221
[2 - SELECT * FROM zipcodes] => 0.00730204582214
[4 - SELECT * FROM zipcodes] => 0.00654602050781
[1 - SELECT * FROM zipcodes] => 0.00626707077026
[5 - SELECT zipcode FROM zipcodes] => 0.0058650970459
[7 - SELECT CONCAT(zipcode, " - ", city) FROM zipcodes] => 0.00581502914429
)
Total number of queries: 7
Total time spend querying: 0.0637030601501
How many duplications:
Array
(
[SELECT * FROM zipcodes] => 4
[SELECT zipcode FROM zipcodes] => 2
[SELECT CONCAT(zipcode, " - ", city) FROM zipcodes] => 1
)
Thanks for reading!
Any comments or suggestions are very welcome!
DB-2-MDB2 in Portuguese
Tuesday, January 16th, 2007Through a trackback I found out that Walter Cruz has translated my DB-2-MDB2 article in a language I was led to believe is Brazilian Portuguese.
Thanks very much Walter, this is very flattering!
Thanks to my buddy Isidoro who enlightened me that the language was Portugeese!
Using PEAR and AWS to keep an eye on Amazon
Wednesday, January 10th, 2007What could possibly be better for a writer's ego other than being read and being praised? Hmm…
So I wanted to have a page that shows the books I've written, together with their Amazon sales rank and the average customer rating and number of reviews. It's really easy. I took one example out of the PEAR book and slightly modified it.
Implementation at a glance
- Get an Amazon Web Services (AWS) subscription id
- Get a copy of PEAR::Services_Amazon package
- Make a request
- Display the response
When making a request, you need to say what type of request it is and what type of response you want. It's documented here. For the type of request, look under "API reference -> Operations" and for the response type, look under "API reference -> Response Groups"
The code
// include the PEAR package require_once 'Services/AmazonECS4.php'; // Your AWS subscription id $subscriptionId = '1WQDAES5PQ**********'; // create a new client by supplying // subscription id $amazon = new Services_AmazonECS4($subscriptionId); $amazon->setLocale('US'); // output options // what do you need returned? $options = array(); $options['ResponseGroup'] = 'SalesRank,ItemAttributes,Reviews'; // for which books // comma-delimited list of ISBNs $items = '1904811795,1904811914,1904811132'; // do the request $result = $amazon->ItemLookup($items, $options); // check for errors if (PEAR::isError($result)) { print "An error occured<br/>"; print $result->getMessage() . "<br/>"; exit(); } // some spaghetti to display HTML response echo '<ul>'; foreach ($result['Item'] as $book) { // loop the books // URL echo '<li><a href="'. $book['DetailPageURL'] .'">'; // book title echo $book['ItemAttributes']['Title'], '</a><br />'; // authors, comma-delimited echo implode(', ',$book['ItemAttributes']['Author']); // sales rank echo '<br />Sales rank: ', $book['SalesRank']; // average rating if (!empty($book['CustomerReviews'])) { echo '<br />Rating: '; echo $book['CustomerReviews']['AverageRating']; echo ', based on '; echo $book['CustomerReviews']['TotalReviews'], ' reviews'; } echo '</li>'; } echo '</ul>'; <a href="#" onclick="javascript:document.getElementById('response').style.display='block';" >Show complete response</a> <pre id="response" style="display: none"> print_r($result); </pre>
Reusing an existing database connection with MDB2
Thursday, January 4th, 2007This is a follow up to a question posted by Sam in my DB-2-MDB2 post. The question was if you can reuse an exisitng database connection you've already established and not have MDB2 creating a second connection.
When using a non-persistent connection
No worries in this case. No new connection will be established. As the PHP manual states:
If a second call is made to mysql_connect() with the same arguments, no new link will be established, but instead, the link identifier of the already opened link will be returned.
That is, if you don't set the fourth parameter to mysql_connect() to true. This parameter forces a new connection. BTW, in MDB2 if you do want to force a new connection, you have to set new_link in the DSN string to true
Bottom line, if you don't do anything special, the existing connection will be reused by MDB2. You can always verify that this is the case by calling phpinfo(INFO_MODULES); and looking in the "mysql" section.
When using a persistent connection
When using a persistent connection you have to do some additional steps to ensure that the same persistent connection is used by MDB2.
- Tell MDB2 that you want a persistent connection -
$mdb2->setOption('persistent', true); - Tell MDB2 which connection you want to use -
$mdb2->connection = $link;, where$linkis your existing connection - Set
$mdb2->opened_persistent = true;
Here's an example:
// somewhere you've established a connection $link = mysql_pconnect('localhost', 'root', ''); mysql_select_db('test', $link); echo $link; // e.g. Resource id #5 // Create MDB2 object require_once 'MDB2.php'; $dsn = 'mysql://root@localhost/test'; $mdb2 =& MDB2::factory($dsn); // reuse your connection $mdb2->setOption('persistent', true); $mdb2->opened_persistent = true; $mdb2->connection = $link; // connect $mdb2->connect(); echo $mdb2->connection; // Resource id #5 // check the "mysql" part to be sure phpinfo(INFO_MODULES);
Laziest image resize in PHP
Wednesday, December 13th, 2006Today I saw a post at digg.com on image resizing with PHP and there was quite a discussion. Let me share the laziest way (that I know of) how to do it - PEAR::Image_Transform is all it takes. Here goes:
require_once 'Image/Transform.php'; $i =& Image_Transform::factory(''); $i->load('test.jpg'); $i->fit(100,100); $i->save('resized.png', 'png');
In addition, the Image_Transform library offers diffferent ways (to skin the old cat) to resize an image - by given pixel value, only on the X axis, on Y, scalling in percentage and so on. And, of course, the library can do much more than resizing, as you can see in the API docs.
It supports all kinds of image manipulation extensions - GD, GD1, ImageMagick, NetPBM, Imlib… If you want to use a specific one, you set as a parameter to the factory() method. In the example above I passed an empty string, so it will try to figure out what's available in my PHP setup and use it, trying Imagick2 first, then GD, then Imlib.
You have setOption() and setOptions() methods if you want to play around with the image quality and those sort of things.
Performance tuning with MDB2
Saturday, December 9th, 2006This is a follow-up to Lars' comment about the PEAR book. In the MDB2 chapter I showed an example how you can create custom debug handlers in MDB2 and then gave a suggestion about a useful application of this functionality for performance tuning. Basically the idea is that your custom debug handler collects all queries that are executed during the life of a given script. Then, once the script finishes execution, the debug handler reports the stats that it has collected. In the book, the example is how you count the number of times each distinct query is executed, this way you can spot problems caused by the OO abstraction. For example, say you have a come class Users that has a method loadUser(), which abstracts the database work. While debugging with the custom error handler, you might figure out that without noticing, you're calling this method in a few places and it makes the same repeating query(queries) over and over again. So you can now optimize/cache results and so on.
The suggestion I made in the book is that in addition to counting, you might want to try executing all SELECTs again, just to see how much time they take and you can execute them once again, prepending them with EXPLAIN to get some details on possible room for improvement.
Now here's one solution to this suggestion. What you can see in this script is:
- Setting up MDB2
- Declaring a custom debug handler class
- "Attaching" it to the MDB2 instance
- Registering it for execution at the end of each script
- Testing it (creating a DB, table, some queries)
I hope you like it and try it out.
Here's the result of executing this script, you can see what you get back.
Room for improvement
Obviously, the method dumpInfo() can be improved. First, it can print out a nice table, instead of lazy print_r(). Then, it can include some logic, my idea is for it to "understand" the EXPLAIN results and to give you a hint by using colors, for exampe green background for queries that are OK, yellow for warnings and red for queries that definitelly need some work. Could be nice, no?
Test script
Kinda longish, but I hope I added enough comments. I also hope I didn't introduce any syntax errors while formatting it for posting here, chopping long lines, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
// PEAR error handling setup require_once 'PEAR.php'; function pearError ($e) { echo '<pre>'; echo $e->getMessage().': '.$e->getUserinfo(); echo '</pre>'; } PEAR::setErrorHandling( PEAR_ERROR_CALLBACK, 'pearError' ); // creating MDB2 instance require_once 'MDB2.php'; $dsn = 'mysql://root:test@localhost'; $mdb2 =& MDB2::factory($dsn); $mdb2->setFetchMode(MDB2_FETCHMODE_ASSOC); // The custom error handler // // It will collect all the queries being executed // in the script, the collection is done by the // collectInfo() method. // Once the script finishes executing, we'll call // the method executeAndExplain() which will // execute all unique SELECTs once again // in order to give us an info of how much time // each query takes. // Then executeAndExplain() will execute again // all SELECTs, this time prepending an EXPLAIN // so that we can get valuable // optimization-related information // Not only that but instead of simple EXPLAIN, // we can use EXPLAIN EXTENDED and after that // we can call SHOW WARNINGS - // this will give us even more optimization hints // // http://dev.mysql.com/doc/refman/5.1/en/explain.html // http://dev.mysql.com/doc/refman/5.1/en/show-warnings.html // class Explain_Queries { // how many queries were executed var $query_count = 0; // which queries and their count var $queries = array(); // results of EXPLAIN-ed SELECTs var $explains = array(); // the MDB2 instance var $db = false; // constructor that accepts MDB2 reference function Explain_Queries(&$db) { $this->db = $db; } // this method is called on every query function collectInfo( &$db, $scope, $message, $is_manip = null) { // increment the total number of queries $this->query_count++; // the SQL is a key in the queries array // the value will be the count of how // many times each query was executed @$this->queries[$message]++; } // print the debug information function dumpInfo() { echo '<h3>Queries on this page</h3>'; echo '<pre>'; print_r($this->queries); echo '</pre>'; echo '<h3>EXPLAIN-ed SELECTs</h3>'; echo '<pre>'; print_r($this->explains); echo '</pre>'; } // the method that will execute all SELECTs // with and without an EXPLAIN and will // create $this->explains array of debug // information // SHOW WARNINGS will be called after each // EXPLAIN for more information function executeAndExplain() { // at this point, stop debugging $this->db->setOption('debug', 0); $this->db->loadModule('Extended'); // take the SQL for all the unique queries $queries = array_keys($this->queries); foreach ($queries AS $sql) { // for all SELECTs… $sql = trim($sql); if (stristr($sql,"SELECT") !== false){ // note the start time $start_time = array_sum( explode(" ", microtime()) ); // execute query $this->db->query($sql); // note the end time $end_time = array_sum( explode(" ", microtime()) ); // the time the query took $total_time = $end_time - $start_time; // now execute the same query with // EXPLAIN EXTENDED prepended $explain = $this->db->getAll( 'EXPLAIN EXTENDED ' . $sql ); $this->explains[$sql] = array(); // update the debug array with the // new data from // EXPLAIN and SHOW WARNINGS if (!PEAR::isError($explain)) { $this->explains[$sql]['explain'] = $explain; $this->explains[$sql]['warnings'] = $this->db->getAll('SHOW WARNINGS'); } // update the debug array with the // count and time $this->explains[$sql]['time'] = $total_time; } } } } // instance of the custom debug handler $my_debug_handler = new Explain_Queries($mdb2); // set debug option $mdb2->setOption('debug', 1); // set debug handler to the method that // collects all queries $mdb2->setOption( 'debug_handler', array($my_debug_handler, 'collectInfo') ); // register functions to be executed on shut down // after the script has finished execution. // Now that the show's over, it's the time to // report what happened in this script db-access-wise // First shutdown function executes the // SELECTs again, the other one prints the results register_shutdown_function( array($my_debug_handler, 'executeAndExplain') ); register_shutdown_function( array($my_debug_handler, 'dumpInfo') ); // // // At this point all MDB2 setup is done, // time for the actual script to do something // // // load the DB manager module $mdb2->loadModule('Manager'); // drop database if it exists // temporarily change the PEAR error handling PEAR::pushErrorHandling(PEAR_ERROR_RETURN); $mdb2->dropDatabase('test_db_explain'); PEAR::popErrorHandling(); // create and set a new database $mdb2->createDatabase('test_db_explain'); $mdb2->setDatabase('test_db_explain'); // create table "events" from a definition array // the table has event ID, name and date/time $definition = array ( 'id' => array ( 'type' => 'integer', 'unsigned' => 1, 'notnull' => 1, 'default' => 0, ), 'name' => array ( 'type' => 'text', 'length' => 255 ), 'datetime' => array ( 'type' => 'timestamp' ) ); $mdb2->createTable('events', $definition); // create a primary key - the ID field $definition = array ( 'primary' => true, 'fields' => array ( 'id' => array() ) ); $mdb2->createConstraint( 'events', 'myprimekey', $definition ); // load the class that has some static helper // functions to work with MDB2's cross-RDBMS // date format MDB2::loadFile('Date'); // INSERT // some data to insert into the events table $data = array( // using MDB2-managed sequences 'id' => $mdb2->nextId('events'), 'name' => "Breakfast a Tiffany's", 'datetime' => MDB2_Date::unix2Mdbstamp( strtotime('Jan 15, 2007') ) ); // The "datetime" value shows how you can use // any date format you wish as long as you're // able to get a unix timestamp out of it // In this case I'm using strtotime() // Then there is a call to MDB2's date helper // to get the MDB2 timestamp // auto insert // for the autoExecute() method we need to // load the Extended module $mdb2->loadModule('Extended'); $result = $mdb2->autoExecute( 'events', $data, MDB2_AUTOQUERY_INSERT ); // // Time to SELECT something // // Using date helpers again $start_date = MDB2_Date::date2Mdbstamp(0,0,0,12,31,1980); $end_date = MDB2_Date::date2Mdbstamp(0,0,0,12,31,2020); $sql = 'SELECT * FROM %s WHERE %s > %s AND %s < %s'; $sql = sprintf( $sql, $mdb2->quoteIdentifier('events'), // quote table name $mdb2->quoteIdentifier('datetime'), // quote field name $mdb2->quote($start_date, 'date'), // quote data as date $mdb2->quoteIdentifier('datetime'), // quote field name $mdb2->quote($end_date, 'date') // quote data as date ); $res = $mdb2->getAll($sql); // execute // // * Bad practice code follows * // Just some more inserts and selects, bad // practice because these queries are not // necessarily portable accross various RDBMS, // lacking proper quoting and preparation for ($i = 2; $i < 31; $i++) { $mdb2->query( 'INSERT INTO events VALUES(' . $i . ', "test event", "2005-05-05 00:00:00")' ); } $res = $mdb2->getCol( 'SELECT DISTINCT datetime FROM events' ); $res = $mdb2->getRow( 'SELECT * FROM events WHERE id IN (SELECT id FROM events WHERE id > 1)' ); |
Personal news update Nov/06
Wednesday, November 1st, 2006So what I've been up to recently? Having a bit of a break, I guess. First, I'm not currently writing a book, after the last one for which I completed my chapter back in June. That's some free time (There is a very exciting book project on the horizon though, we'll see). Then, I'm not working so much on our new house. Now (August) that my family is back from Bulgaria, and the two little princesses are running around, it's next to impossible to do any construction work, however small.
I changed my job not so long ago, I'm working for SAP now, here in good old Old Montreal. Meeting smart people every day and learning new things, SAP has quite a bit going on and there's always something to learn. Take the proprietary ABAP programing language for one.
As always a PHP junkie, I looked into what's possible in terms of integration of PHP and SAP. Turns out it's possible and it's fun. There is this open-source SAPRFC PHP extension, that allows you to use PHP to connect to an SAP system and do stuff. Out of this interest a few things happened:
- I published an article at the International PHP Magazine about a tool (or more like a collection of tools), called Scripting In A Box which is developed at SAP. It's one big archive (as in ZIP) which you unpackage to your C:\ drive and you get Apache, MySQL, PHP(+SAPRFC), Perl, Ruby/ROR, Python, Eclispse(+PHPEclipse), all pre-configured and running together. So you can start scripting in minutes. This tool actually gave me a chance to try out and love PHPEclipse, something I've missed, being so attached to my TextPad and ignoring any other way to do PHP. Now I can highly recommend PHPEclipse as a PHP IDE.
- Next, some folks at SAP (Thanks Craig, André!) recognized my PHP experience and asked me to do a little demo of SAP+PHP at SAP's big event, called TechEd in Las Vegas. This was quite an experience! Las Vegas is one different place and the conference itself was pretty big with 5000 people, I think. I had a chance to meet guys from SDN (SAP Developers Network), which is quite a vivid community with something like half a million members. You know what's the everage response time when you post a question on the SDN forums? 7 minutes.
- Then, I wrote another article for IPM, which described the demo I did at TechEd. (I'll add the source code and some screenshots at the bottom of this post.)
- I also contributed one new container for the PEAR::Auth package, it allows you to authenticate users against an SAP system in your PHP app.
- Another contribution to PEAR was the ABAP language definition for the Text_Highlighter package
- Finally, I did a little ABAP console, but I'll blog about it seperately and will share the code, of course.
- … and my first posting on SDN was published today. The next one will be a sort of a cross-post here and on SDN about the ABAP console thingie.
I think that's about it for SAP. Otherwise, as usual, I get easily excited by different things, so I've been doing this and that, here and there, on my own terms, relaxing, without any deadlines preasure.
On the pipeline, I have a bit of stuff to do, again, small little things I enjoy doing, like helping with one article for the PEAR::MDB2 manual, assembling an extra intro chapter for the PEAR book, helping out with Text_Highlighter (I'm this package's official helper since a few days ago), also doing some work for the Image_Text PEAR package, as well as anything else that comes into the radar any given day. Yeah, this is how I understand relaxation, doing whatever you're passionate about, even if this means less sleep at night. Ah, and I have the test for Canadian citizenship tomorrow, so I should be reading as opposed to writing now (I always do this, the busier I am, the more interesting things I "shoehorn")
SAPRFC/PHP demo files
Yeah, the demo app uses YUI and a bit of AJAX and animation to make it a bit sexier.
JSON renderer for Text_Highlight
Friday, October 27th, 2006Text_Highlighter is one of my favourite PEAR packages, seems like I'm addicted to highlighting source code. After adding BB code and simple HTML renderers and an ABAP code syntax definition, today I played with adding a JSON renderer. Useful in case you want to get highlighted source code in your new shiny AJAX app.
Array renderer
After I did the JSON renderer, I said, OK, what if I want to tweak the JSON output just a bit (or the output from any renderer for that matter)? Add more options? Nah, I had a better idea, I scrapped the whole thing and did an Array renderer first. If you have the array output from the renderer, it's trivial to format it as JSON, or XML, or HTML, or anything. I believe even the exisitng Text_Highlighter renderers should be rewritten, to extend an Array renderer. Anyway, back to JSON.
Demo
To see the JSON renderer in action, you can go to my hiliteme.com site and check the JSON tab.
Source
The source code is available here - JSON.phps which extends Array.phps. To test, you need to add the two renderers to your PEAR repository under Text/Highlighter/Renderer
Example
So let's say you need to highlight the PHP code
<?php
echo "Hello Highlighted World!";
?>
You create an instance of Text_Highlighter and Text_Highlighter_Renderer_JSON and call the highlight() method, assuming that the code you need highlighted is in $source
// dependencies require_once 'Text/Highlighter.php'; require_once 'Text/Highlighter/Renderer/JSON.php'; // instance $json_options = array(); $json_options['tabsize'] = 4; $json_options['enumerated'] = true; $renderer =& new Text_Highlighter_Renderer_JSON($json_options); $highlighter =& Text_Highlighter::factory($_POST['language']); $highlighter->setRenderer($renderer); // do the highlighting $json_result = $highlighter->highlight($source);
Now $json_result will look like:
[["inlinetags","<?php"],["code"," \n "],["reserved","echo"],["code"," "],["quotes","""],["string","Hello Highlighted World!"],["quotes","""],["code","; \n"],["inlinetags","?>"]]
As you see the JSON output is an array, one element per highlighted keyword, and in this array there is a sub array - class/keyword. If you want to display this in your page (let's say you got it from an AJAX call), you can do a loop through the array and surround the keywords with span tags of the selected style:
// say your ajax call returned var src // var src = xmlhttp.responseText; var data = eval(src); var res = ''; for (var i in data) { res += '<span class="hl-' + data[i][0] + '">'; res += data[i][1]; res += '</span>'; } var el = document.getElementById('some-div'). el.innerHTML = '<pre>' + res + '</pre>';
Here I used innerHTML, you can also use DOM, but in this case you need a special case for the "\n" so that you can create a br element to address IE's habit of ignoring line feeds in a DOM-generated pre tag.
BTW, if you don't set the enumerated option to true, you'll get objects inside the main array, check hiliteme.com's JSON tab for an idea of how this would look like.
The PEAR book
Sunday, October 15th, 2006
In case you've missed it - the PEAR book hit the streets! The exact title is "PHP Programming with PEAR" and it's co-written by Stephan Schmidt, Carsten Lucke, Aaron Wormus and yours truly. Aaron also put up a Wiki for book and PEAR-related updates, it's at thepearbook.com
I tried to put up a list of the packages and classes covered in the book, I've probably missed some classes, especially Date_* and Calendar_* ones, but I hope I got all the packages. Here goes (alphabetically) :
- Calendar
- Date
- Date_Holidays
- Date_Span
- Date_Timezone
- File_PDF
- HTML_Table
- HTML_Table_Matrix
- HTTP_Request
- MDB2
- MDB2_Schema
- Services_AmazonESC4
- Services_Ebay
- Services_Google
- Services_Technorati
- Services_Webservice
- Services_Yahoo_Search
- Spreadsheet_Excel_Writer
- Structures_DataGrid
- Structures_DataGrid_Column
- Structures_DataGrid_DataSource
- XML_Beautifier (mention)
- XML_FastCreate
- XML_Parser
- XML_RPC
- XML_RPC_Client
- XML_RPC_Message
- XML_RPC_Response
- XML_RPC_Server
- XML_RPC_Value
- XML_RSS
- XML_Serializer
- XML_Util
- XML_XUL
For more info on a package, you can consult the PEAR site and manual. Did you know that you can access a package's page by typing its name (case insensitive) in the URL after pear.php.net, like http://pear.php.net/mdb2_SCHEMA for example?
ABAP code syntax highlighting
Tuesday, September 19th, 2006Just finished and eager to share - I added a new syntax definition to the Text_Highlighter PEAR package (see also here). It's for highlighting code written in the SAP's own ABAP programming language.
A live demo is available at the hiliteme.com site, just pick ABAP from the drop-down of programming languages. Any feedback is appreciated, because it's a brand new thing and may have bugs or incompletenesses (is that a word?). So feel free to highlight your ABAP code and post it to blogs or forums.
The implementation wasn't hard, the Text_Highlighter package is made to be extended and even provides the tools for that. All you need to do is create an XML file that contains keywords and other syntax rules, such as formats of the comments and so on. Then there is a command line tool that takes the XML file and generates a class out of it. The class is later on used when highlighting. Here's the XML file in case you want to improve on it and generate your own ABAP.php class:
The PEAR book is on it's way
Tuesday, September 12th, 2006
Here's the link to publisher's page dedicated to the PHP Programming with PEAR. Guess who wrote the chapter for MDB2?