PEAR and CakePHP

This post is about my experience with loading in PEAR to a CakePHP 1.2.x application. This may be the right way or the wrong way, but I got it to work throughout the application. I had to do some changes, and if there is a better way of doing this, please let me know.

First off, here is the issue. I needed to be able to export a group of records from the database to an excel spreadsheet. I have tried to use the Excel Spreadsheet add in that is listed on the Bakery. It works nice, and I had to do some modification for 1.2, but it worked. But not the way I wanted it. I have used the PEAR library Spreadsheet_Excel_Writer before and I like the type of control that I wanted, over the cells, the formatting, the merging, etc etc etc. It provides the type of control that I wanted. So here is what I did to get this to work with the CakePHP framework.

First, I have to download the PEAR library and the Spreadsheet_Excel_Writer libraries to use. Since I use a local system to help develop, I could download these libraries to the local system and transport these over to the CakePHP application. So I went to PEAR site to get the libraries. To download these I ran the following commands:

pear install PEAR-1.8.1
pear install OLE-1.0.0RC1
pear install Spreadsheet_Excel_Writer-0.9.1

URL’s are listed below:
http://pear.php.net/package/PEAR/download
http://pear.php.net/package/Spreadsheet_Excel_Writer/download
http://pear.php.net/package/OLE/download

These downloaded the to local drives and I copied them over to the CakePHP area. Here is where it gets a little tricky. And I thank “brian” who helped me on the Cake Google group to get past this when I got into a problem. So here we go, diving in to this.

First off, the PEAR libraries need to be put in the vendors directory. If you look at the directory structure for Cake, it appears like this:
/app
/cake
/vendors

Inside of the /app directory, there is another vendors directory. This is where I put the PEAR libraries. This causes problems, because in the /cake/config/paths.php file, there is a path defined for VENDORS, and for PEAR:

if (!defined('VENDORS')) {
	define('VENDORS', CAKE_CORE_INCLUDE_PATH.DS.'vendors'.DS);
}

define('PEAR', VENDORS.'Pear'.DS);

Now, I have put the PEAR libraries in the top level vendor directory. If you choose to put it in the app/vendors directory, then you may need to change a core file, which is not advisable, because you would need to change the path above to

if (!defined('VENDORS')) {
	define('VENDORS', CAKE_CORE_INCLUDE_PATH.DS.'app/vendors'.DS);
}

So back to the PEAR libraries. Here is what I needed to do. I moved the PEAR directories to the /vendors directory. So here is what that directory looks like:

/vendors
    /css
    /js
   /Pear   <--- Look at this, case sensitive based on those paths above
        /OLE
        /OS
        /PEAR
        /scripts
        /Spreadsheet
        INSTALL
        LICENSE
        OLE.php
        package.dtd
        PEAR.php
        PEAR5.php
        README
        System.php
        template.spec
    /shells

This is part of the PEAR install we need to do. Now we need to update the paths in some of these files so that CakePHP can find them and include them.

**** ORIGINAL ENTRY THAT HAS BEEN EDITED/DEPRECATED ********************
**** EDIT: Based on the original post this is the method I originally used. This way works, but requires a
**** little too much overhead and editing the files in the PEAR libraries which is never really a good idea
**** and should only be used sparingly. To see the way that it should be done, please see below this
**** section.
****
**** In the file /vendors/Pear/Spreadsheet/Excel/Writer.php there is 2 requires
**** require_once 'PEAR.php';
**** require_once 'Spreadsheet/Excel/Writer/Workbook.php';
****
**** In order for the Cake App to see these, at least in my set up, I needed to change these to the
**** following:
**** require_once 'PEAR.php';
**** require_once PEAR . 'Spreadsheet/Excel/Writer/Workbook.php';
****
**** I needed to add "PEAR . " to the require_once call. Now I needed to add this to the following files:
**** /vendors/Pear/Spreadsheet/Excel/Writer.php
**** require_once 'PEAR.php';
**** require_once PEAR . 'Spreadsheet/Excel/Writer/Workbook.php';
****
**** /vendors/Pear/Spreadsheet/Excel/Writer/Workbook.php
**** require_once PEAR . 'Spreadsheet/Excel/Writer/Format.php';
**** require_once PEAR . 'Spreadsheet/Excel/Writer/BIFFwriter.php';
**** require_once PEAR . 'Spreadsheet/Excel/Writer/Worksheet.php';
**** require_once PEAR . 'Spreadsheet/Excel/Writer/Parser.php';
**** require_once PEAR . 'OLE/PPS/Root.php';
**** require_once PEAR . 'OLE/PPS/File.php';
****
**** /vendors/Pear/OLE/PPS.php
**** require_once 'PEAR.php';
**** require_once PEAR . 'OLE.php';
****
**** This had helped the application find my PEAR libraries when trying to do this
**** END ORIGINAL ENTRY ***********************************************

Now, a word about the edit. The above method works, but is not a preferred method. The best method for this, so there is no need to edit PEAR library files is the following. And a big thanks to Daniel Hofstetter for pointing this out.

I put this at the top of my controller file. I am sure there is better places for this, probably even the app_controller file so that all controllers get the needed include path set. Here is what I did.

I needed to append the include path so that the new PEAR path would be found. After the php opening, I added this line:

ini_set("include_path", PEAR . PATH_SEPARATOR . ini_get("include_path"));

I am going to go over this just a little. First off, the ini_set is called to set the include_path. But we do not want to destroy any other include paths that are set up as well. So when we add the PEAR path, we need to also include the other paths as well. So the initial include_path was as follows (given in example form only, where the directory "test" is where I have CakePHP installed)
ini_get("include_path") =

/www/htdocs/html/test:/www/htdocs/html/test/app/:.:/usr/local/php5/lib/php

Since CakePHP already defines the variable for the PEAR path as "PEAR", we can use that to add to the include path, like shown above. After setting the path using ini_set(), we run ini_get("include_path") it would =

/www/htdocs/html/test/vendors/Pear/:/www/htdocs/html/test:/www/htdocs/html/test/app/:.:/usr/local/php5/lib/php

By doing it this way, there is no need to edit the PEAR library files, and we can add new PEAR libraries without having to worry about editing those files as well.

Now, I needed to make the controller aware of the vendor library. In my controller file I added this line before the class declaration:

App::import('vendor', 'Spreadsheet_Excel_Writer', array('file' => '../vendors/Pear/Spreadsheet/Excel/Writer.php'));

In Cake 1.2, this is how the vendor's are imported. The vendor() declaration has been deprecated. This imports a vendor, gives the class a name (I choose the base one that it is usually called), and the location of the of the file. In my set up, I needed to add the "../", you may not have to.

In the function, (I called "export"), I did not want to have a "view" page for it. The first thing I did was grab the information I needed. For this example, I needed all users that signed up for a conference. So I grabbed that information and put it in an array $registrations.

function export ($id = null){
       // I only want to get a specific conference, not all of them
	if ( $id == 'all' ){
		$this->Session->setFlash('Please select a specific conference to export the registrations.');
		$this->redirect(array('action' => 'index')); 
	}

	// Now get the registrations for the conference
	$registrations = $this->Registration->find('all',
		array(
			'conditions' => array('conference_id' => $id),
			'fields' => array('*'),
			'recursive' => '-1',
			'order' => array('Registration.created'),
		)
	);

Now comes the fun part, building the column heading array, and then instantiating the writer

	// Set up the header array
	$titles = array(
		'Name' => 15,
		'Address' => 20,
		'City' => 20,
		'State' => 7,
		'Zip Code' => 10,
		'Email' => 20,
		'Phone' => 13,
	);
	
	$rn = 0; // row number	
	// Build the XLS file using PEAR
	$xlsBook = new Spreadsheet_Excel_Writer(); 
	$xlsBook->send("registrations.xls");
	$xls =& $xlsBook->addWorksheet('Registrations');

Everything else is now just as the same as it would be with the Spreadsheet-Excel_writer. Create the formats as you would like, for text, numerics, specialized strings, colors, etc. Write the sheet headings, if you so desire

	/* Create styles for the spreadsheet */
	$format_bold =& $xlsBook->addFormat();
	$format_bold->setBold();
	
	$main =& $xlsBook->addFormat(
		array('Size' => 14,
			'Align' => 'center',
			'Color' => 'black',
			'Bold' => 'true'
		));
	$main->setBold();
		
	$formatText =& $xlsBook->addFormat(array('Size' => 11));
	
	$cn = 0;
	$xls->write($rn, 0, "CONFERENCE REGISTRATIONS", $main);
	$xls->mergeCells($rn,0,$rn,11);
	$rn++;

As you can see, just use the writer calls to write the data, format it, and do what you need. To get more information on this, please check the PEAR documentation for this library.

Finish up the column headings by doing a quick little loop

	// Set up the headings of the columns
	foreach ( $titles as $t => $val){
		$xls->setColumn($cn, $cn, $val);
		$xls->write($rn, $cn++, $t, $format_bold);
	}
	$rn++;
	// reset the column num
	$cn = 0;

Now you can do the actual rows in a loop:

	foreach ( $registrations as $r ){
		$xls->write($rn, $cn++, $r['Registration']['name'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['address'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['city'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['state'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['zip_code'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['email'], $formatText);
	    $xls->write($rn, $cn++, $r['Registration']['contact_phone1'], $formatText);
	    // cycle to the next row
	    $rn++;
	    // Reset the column 
	    $cn = 0;
	}
	
	$xlsBook->close();			
	exit();
} // end of function

Now, this does the work for me on my code. To call it in the view, I have a page that shows all registrations on the page, the function name is "registrations". I set a variable in this function for the ID number to be passed to the view. In the view for this function, I have put the following:

if ( $param != 'all') {
	echo "<p><br />&nbsp; &nbsp; &nbsp; - - <b>";
	echo $html->link(__('EXPORT DATA', true), array('action' => 'export', $param) );
	echo "</b> - - <br /><br /></p>";
}

So I want a specific conference. If there is not one, and they are viewing all registrations for all conferences, then it does not show the link. But if it is a specific id, then it shows the link to export with the corresponding parameter for the ID.

And there it is. Using the Spreadsheet_Excel_Writer PEAR library with CakePHP 1.2.

Again, this works for me, and there may be a better way of doing things, and if so, please feel free to tell me. I am always looking for new things to learn.

17 thoughts on “PEAR and CakePHP”

  1. Wouldn’t it be easier to add the “pear”-path to the include_path, so you don’t have to edit the pear files? Something like ini_set(‘include_path’, $pearPath . PATH_SEPARATOR . ini_get(‘include_path’));

  2. Yes, that would be a great way to do it. I did not want to edit the PEAR files initially, but was working on a few projects at the same time and did this as it solved my problems, albeit in an “out-there” method.

    That is a great suggestion. I will need to go back thru my code and update this.

    When I get it fixed later I will update the post to reflect this way as well. Thank you for the suggestion.

  3. Thanks for this – I was looking for a XLS creation technique and happened to stumble across this on planet cakePHP. Editing the PEAR files was a pain so I look forward to the updated instructions!

  4. Hi again Stephen… figured I’d update you on this since I found your post so helpful.

    I’ve been working with this and found what I believe is a more “cake like” or more “true” MVC method of rendering the view for the excel spreadsheet. I am building a reporting system for my app and I soon found my “reports” controller was getting really full of many functions to create the various spreadsheets. I figured that really the spreadsheet exports are actually views so the code for constructing them should be in a view file.

    This is what I’ve done:

    1) Copied the empty ajax.ctp layout from ./cake/libs/view/layouts/ into ./app/views/layouts/ and renamed it excel.ctp.

    2) Used $this->layout and $this->render in the controller to send requests for reports (i.e. the excel spreadsheets) to a specific view file using the “excel” layout. For example:

    // either export to excel or show the normal view
    if ($export != ‘export’) {
    $this->render(‘report_name’);
    } else {
    $this->layout = ‘excel’;
    $this->render(‘report_name_export’);
    }

    In my case I have the “report_name.ctp” view which shows the report on screen normally and the “report_name_export.ctp” view which exports the report to excel using the method you described.

    Each “export” view must include the line to import the Spreadsheet_Excel_Writer library:

    App::import(‘vendor’, ‘Spreadsheet_Excel_Writer’, array(‘file’ => ‘../vendors/Pear/Spreadsheet/Excel/Writer.php’

    (I tried to put this in the excel.ctp layout but it didn’t seem to work, no idea why)

    This makes it much cleaner IMO and for me at least it’s much easier to work with every export in the individual view file rather than clogging up my controller!

    Best wishes
    Chris

  5. Chris, I like the approach. You are correct in that in a true MVC approach, it should be in the view. Since my approach was just for 1 report, it clogs the controller only a little, but I still ought to change this around according to your suggestion.

    Maybe, to be even cleaner in the view, we could take the Excel object, put it in a helper, with certain functions, so that the excel writer will have its code in a central place and called in various views to build different excel reports.

    Great suggestion Chris.

  6. Hi, Im trying to make this work.. Im getting an error which says, “Warning (2): require_once(Console/Getopt.php) [function.require-once]: failed to open stream: No such file or directory [CORE/vendors/Pear/System.php, line 21]”

  7. Hi, its me again.. I was able to make it work.. it generates the excel file but then the contents of the worksheet isnt right.. its full of special characters..

    help please?

  8. With the special characters, what are you trying to output, and are you sure that you are sending the data out in UTF-8? Microsoft Excel usually tries to do what it thinks is best, which is most of the time wrong. So make sure the encoding is correct.

    Also, if there is an error in the logic before it outputs, like calling a variable that is not there, it could send out an error, and send special characters.

  9. All the steps I did are listed out here. I modified one step, but I called that out in the post, and highlighted it. When I get a spreadsheet full of special characters, that usually means that there was an error somewhere in the code that I needed to go back and correct.

  10. per francis’s problem: Hi, Im trying to make this work.. Im getting an error which says, “Warning (2): require_once(Console/Getopt.php) [function.require-once]: failed to open stream: No such file or directory [CORE/vendors/Pear/System.php, line 21]“

    My solution was to comment out line 21 in System.php. I did the “modify the core files” nono, but don’t know what other solution to take…

  11. this works on my windows local server but it sadly brefuses to work on my linux live server…it keeps telling me Fatal Error: Class Spreadsheet_Excel_Writer not found in controller line 80…been trying to get it to work for the past few days with no luck.

Comments are closed.