Thursday, February 20, 2014

Building a basic Hello World module in Magento 1.8

To create a small "Hello World" module in Magento is very easy. This would require a Namespace (say, "Test") and ModuleName (say, "Helloworld"). Next follow these steps.

1. Create an XML file in app/etc/modules folder called "Test_Helloworld.xml". This name has a format {namespace name}{underscore}{module name}
   
  Write the following code in it :: 
  
  <?xml version="1.0"?>
 <config>
  <modules>
   <!-- Specify the NamespaceName_ModuleName -->
<Test_Helloworld>
     <!-- If we need to Disable it, we do it here -->
    <active>true</active>
    <!-- Which CodePool to Use -->
     <codePool>local</codePool>
</Test_Helloworld>
  </modules>
 </config>

  Magento has 3 codepools namely "core", "community" and "local". We won't be touching the "core", rather we would be creating our modules in "Community" or "local" codepool. If we choose "community", we need to change one line in the above XML to "<codePool>community</codePool>". Here we are creating modules in "local" folder.
  
  This XML is needed to tell Magento that we are going to add a new Module and this module's code can be found inside "local" codepool. Now we can see our new module to appear in the admin panel. In System > Configuration > Advanced > Advanced section, you'll that our new module is listed there.





2. Now, we need to create a folder with the Namespace name "Test" inside app/code/local folder

3. Next, we need to create a folder with Module Name "Helloworld" inside the "Test" folder

4. Now create 2 subfolders "controllers" and "etc" inside app/code/local/Test/Helloworld/

5. Next we need another configuration file called config.xml ( inside 'etc' folder ) where all the module's basic configuration will be stored. Here is the content for app/code/local/Test/Helloworld/etc/config.xml ::
   
  <?xml version="1.0" encoding="UTF-8"?>
  <config>
    <modules>
     <!-- Again, NamespaceName and ModuleName -->
     <Test_Helloworld>
      <!-- Not too much of importance here -->
      <version>1.0.0</version>
     </Test_Helloworld>
    </modules>
    
    <frontend>
     <routers>
     <!-- How we are going to access the module from front-end? Router name is "helloworld" -->  
       <helloworld> 
         <use>standard</use> 
           <args>
   <!-- Which module will start to work for the 'helloworld' router -->
              <module>Test_Helloworld</module> 
              <frontName>helloworld</frontName> 
           </args>
        </helloworld>
      </routers>
    </frontend>
  </config>

8. Next, we create the main controller IndexController.php inside app/code/local/Test/Helloworld folder. Check its content ::

  <?php
 class Test_Helloworld_indexController extends Mage_Core_Controller_Front_Action
 {
   // Default action 
   public function indexAction()
   {
     echo " <b>Index Action</b>";
   }
 
   // Another action called 'view'
   public function viewAction()
   {
     echo " <b>View Action</b>";
   }
 
 }
 ?>

Here, we have created a class with naming convention {NamespaceName}_{ModuleName}_{ControllerName}. Then we create two actions called "indexAction" ( which is default action ) and "viewAction". Check relationships between URLs and these actions below ..
     
http://localhost/magento/helloworld                :: Index Controller > index Action
http://localhost/magento/helloworld/index        :: Index Controller > index Action
http://localhost/magento/helloworld/index/view      :: Index Controller > view Action
http://localhost/magento/helloworld/index/view/index :: Index Controller > view Action
    
Check the Output below :




If we want to load default template and theme onto our page, the index Controller's actions must be changed as shown below :

<?php

class Test_Helloworld_indexController extends Mage_Core_Controller_Front_Action
{
  // Default action 
  public function indexAction()
  {
      $this->load_theme();
      echo " <b>[Index Action]</b>";
  }
 
  // Another action called 'view'
  public function viewAction()
  {
$this->load_theme();
echo " <b>[View Action]</b>";
  }
 
  // Another action 
  public function say_hiAction()
  {
$this->load_theme();
echo " <b>[Say_Hi Action]</b>";
  }
 
  // Private Function
  private function load_theme()
  {
$this->loadLayout();
$this->renderLayout();

// Print Template and Theme Information
echo "Current Theme Information : Package [" . Mage::getSingleton('core/design_package')->getPackageName() . "], Theme : [";

echo Mage::getSingleton('core/design_package')->getTheme('frontend') . "]";
  }
}
?>
  
 We have created a private function load_theme() which loads layout and renders it on the browser. It also prints the current Package and Theme name on the screen.
    
A new public function say_hiAction() has also been added. So if we call http://127.0.0.1/magento1.8/helloworld/index/say_hi, Index Controller's say_hiAction() function will be called. 
   
Check the Output below :





The Template and Theme information is printed at the top of the screen.  The statements $this->loadLayout()and $this->renderLayout() loads and renders the templates on the browser.

Magento Notes - II

Let's discuss some more Magento 1.8 stuffs ::

1. On your Cart page, how to get each product's category names?
    
   Put the following code where you want to show Category Name in  app\design\frontend\YOUR_PACKAGE\YOUR_THEME\template\checkout\cart\item\default.phtml   

<?php
   // GEt Product ID
   $proid = $this->getProduct()->getId();

   // GEt Category IDs
   $ids = $this->getProduct()->getCategoryIds();

   // Show Category Names
   $cat_name = "";

   foreach($ids as $val) 
   {
$cat_name .= Mage::getModel('catalog/category')->load($val)->getName() . ", ";
   }
   echo "Category : " . rtrim( $cat_name, ", ");
 ?>

2. How to get custom text attribute of a product?

   Suppose you have created a new text attribute called 'custom_desc'. Now, on various pages, you want to get this attribute's value. Insert any of the following codes to get it done.

   <?php
  // Method 1
  $custom_desc =  $_product->getData("custom_desc"); 
   
  // Method 2
  $custom_desc = $_helper->productAttribute($_product, nl2br($_product->getCustomDesc()), 'custom_desc'); 
   
  // MEthod 3 :: Easiest
  $custom_desc = $_product->getCustomDesc(); 
 ?>
   
   Amazing is the fact that if we create a property/attribute called "test", calling "$_product->getTest()" would return value of that property for the product "$_product". Such custom named functions will be automatically available with the product object. Check more examples below.
   
   "short_description"      => $_product->getShortDescription()
   "country_of_manufacture" => $_product->getCountryOfManufacture()
   
3. How to get Customer’s Email Address into the Order Notification Email Templates ?
  
  You need to insert the following shortcode into various email template files stored in app\locale\en_US\template\email\sales\ folder.
  
  {{htmlescape var=$order.getCustomerEmail()}}
  
4. How to identify whether current page is the home page?
   
   Use the following code snippet to help ...

   <?php
   
  $is_homepage = Mage::getBlockSingleton('page/html_header')->getIsHomePage();  
   if( $is_homepage )
   {
      // Do, if Home Page
      // However this won't work if you are sitting
      // on Home Page, but you have extra slashes in
      // URL like abcd.com/?p=2 or abcd.com/home?page=2
   }
 ?>

Another most trustworthy solution is ::

   <?php
  if( Mage::getSingleton('cms/page')->getIdentifier() == 'home' )   {
      // Do, If Home Page
      // This would work most of the times as usually 'home'
      // identifier (for page) is what we always set as Home
  }
 ?>  

5. What to do when Products are not showing up in Category page or Search results?

    1. Check if product is enabled
    2. Check product Quantity is valid. However if "Display out of stock products" is set to "yes", then products will be shown even if it has zero quantity.
    3. Check if product is assigned to a category
    4. Check Attribute settings, check if product attributes like "name" etc are "Searchable"
    5. Check if "name" attribute is having a "Global" scope
    6. Clear Cache
    7. Re-Index all the indexes

6. How to get Stock Quantity of the current product?
    
Use the following code to get current product's stock quantity. You may replace the product object "$_product" with other object you are working with.

   <?php
    $_quantity = intval(Mage::getModel('cataloginventory/stock_item')->loadByProduct($_product)->getQty());
 ?>

Check out Magento Notes - I

Magento Notes - I

Here I discuss some quick tips for Magento 1.8. 

1. How to set "One-column" template to Category Page?

   a. If you use 'base' package's 'default' theme, open app/design/frontend/base/default/layout/catalog.xml 
   
   b. If you have defined your own theme in 'default' package and overridden the above file, then open app/design/frontend/default/YOUR_THEME/layout/catalog.xml 

   Locate the following :: 
   
  <catalog_category_default translate="label">
    <label>Catalog Category (Non-Anchor)</label>

   Add the following ::  

   
  <reference name="root">
<action method="setTemplate">
             <template>page/1column.phtml</template>
         </action>
  </reference>

2. How to Resize Image in Product Listing Page ?
  
   Open app/design/frontend/YOUR_PACKAGE/YOUR_THEME/catalog/product/list.phtml

   Add the following <image> tag where necessary. We are resizing image to 218x317 (width x height). Product object "$_product" is already available to you.

   <img src="<?php echo $this->helper('catalog/image')->init($_product, 'small_image')->resize(218,317); ?>" width="218" height="317" alt="<?php echo $this->stripTags($this->getImageLabel($_product, 'small_image'), null, true) ?>" />

3. How to Resize Image in Product Details Page ?

   Open app/design/frontend/YOUR_PACKAGE/YOUR_THEME/template/catalog/product/view/media.phtml

   Add following <image> tag where necessary.

   <img id="image" src="<?php echo $this->helper('catalog/image')->init($_product, 'image')->resize(338,438);?>" alt="<?php echo $this->escapeHtml($this->getImageLabel());?>" title="<?php echo $this->escapeHtml($this->getImageLabel());?>" />

4. How to call a static block from inside the phtml file?

   If we have already defined any static block in Admin Panel, we can call it from within view (.PHTML) files. We just need to have its identifier. Then use the following code where necessary.
  
   <?php 
  echo $this->getLayout()->createBlock('cms/block')->setBlockId('STATIC_BLOCK_IDENTIFIER')->toHtml(); 
 ?>
   
   Make sure you replace the 'STATIC_BLOCK_IDENTIFIER' with the real one.

5. How to fix the error "TypeError: productAddToCartForm is undefined" Magento?
  
   This Javascript error may crop up on your product details page when "Add to Cart" to button is clicked. This happens because of the conflict between existing Prototype JS library and jQuery (which you added later).

   Simple solution is to add "jQuery.noConflict();" statement after you have included your jQuery Library file.
   
6. How to get current product's Category name?

   I assume that you have $_product object at your disposal. Then, use the code below to get Category names. A product can belong to multiple categories. 
   
<php 
  // Get All Category IDs
   $cat = $_product->getCategoryIds(); 

   // Get First Category Name
   $cat_name = Mage::getModel('catalog/category')->load($cat[0])->getName();
?>

For More, check out my next article Magento Notes - II.

Monday, February 17, 2014

Compare two Databases with PHP

Very recently I faced a big problem where I had a older DB working with new version of files. To be precise, I was working with Magento 1.8 files with Magento 1.6 database filled with various products and customer information. And frankly speaking, it landed me into deep water as various stuffs like shopping cart, checkout process, reIndexing services, catalog price updation etc started to give errors. It all happened just because new magento files are expecting new fields in the DB. So, I wrote a piece of code to match the original DB (v1.8) with my current working DB (v1.6). And very soon I was able to track down the table differences in two databases and I just manually added those extra columns which the current DB (v1.6) was lacking. Soon, all the errors on the site mostly vanished. 

Manually column addition is a bad idea because it does not create the Indexes which already exist in correct/Original DB. For that, I'll be writing another script. Till that time, check out the php code for this simple Database comparison.

Download table_comparison.php

This solution works the following way :: 

1. We connect to 2 Databases within a script. If the server allows multiple DB connection on a single script, then there won't be any problem. Otherwise we need to use a fourth parameter (true) with 2nd mysql_connect() onwards.
   
 mysql_connect( db_host, db_username, db_password, true );

2. We use a "SHOW TABLE"  query to fetch all the tables from a Database and store them in an Array. This way we create 2 arrays, first one holding all the table names from DB1 and the second one holding all table names from DB2. So, if a DB has 300 tables and another has 400 tables, that differences can be easily found by counting elements in each array.

3. Now, we would iterate through the first array (holding table names from correct database DB1); taking each table name we would run a "DESCRIBE table_name" SQL and fetch all the column names and store them in an array. Now we would do this for each table in each database and then we would match column names.

4. Reporting would be a very essential thing. We need to use various flags/counters/Strings etc to keep track of changes and use them in generating the report.

Check out some screenshots showing the output ::

























Friday, February 14, 2014

File download script using PHP

Opening a file in another window or tab within a browser, we can use an anchor to that file like this : 

<a href="test.pdf" target="_blank">Download the PDF here</a>

it will open the file in a new tab. To force the browser to download it on the viewer's PC, we need to write some server side code. Here I am showing some sample codes which would show "Save As" message box on browsers.

Setting the headers is the most important thing here. Here, code for downloading PDF file is shown. We are reading "sample.pdf" and forcing the browser to download it as "test.pdf" on viewer's machine. We can have variety on our solutions.

Solution 1 ::

<?php

/// Set the header
header("Content-type:application/pdf"); 
header("Content-Transfer-Encoding: binary");
header("Content-Disposition:attachment; filename=test.pdf");

/// Deliver the content
readfile("sample.pdf");
?>

Solution 2 ::

<?php

// GET the Source file's contents
$str = file_get_contents("sample.pdf") or die("Could not open file");

// Get the Target
$fp = fopen("php://output","w+") or die("Could not open output stream");

// Set the Header
header("Content-type:application/pdf"); 
header("Content-Transfer-Encoding: binary");
header("Content-Disposition:attachment; filename=test.pdf");

// Send Data
fputs($fp, $str);

// Close file
fclose( $fp );
?>

Solution 3 ::

<?php

// GET the Source file
$fp = fopen("sample.pdf","r+") or die("Could not open file");

// Set the Header
header("Content-type:application/pdf"); 
header("Content-Transfer-Encoding: binary");
header("Content-Disposition:attachment; filename=test.pdf");

// Read and Send Data
while( !feof($fp) )
 echo fread( $fp, 1024 );

// Close file
fclose( $fp );
?>

All the above solutions would work smoothly but the first one needs output buffering to be enabled. The 3 solutions above have different file reading and rendering methods only, the header information remain same. We are reading a file called 'sample.pdf' and forcing it to be downloaded with a default name 'test.pdf'. 

The "Content-Disposition:attachment" is very important. This would prompt for downloading the file instead of directly showing it in Browser.  

We can force the browser to download other file types like Excel file but for that we need to modify the MIME 'Content-type' in response header. For an Excel file to be downloaded we may use the following code :

<?php

// GET the Source file
$filename  = "tester.xls";
$fp = fopen($filename,"r+") or die("Could not open file");

// Set the Header
header("Content-Type:application/vnd.ms-excel; charset=utf-8"); 
header("Content-Transfer-Encoding: binary");
header("Content-Disposition:attachment; filename=genuine.xls");

// Read and Send Data
echo fread( $fp, filesize($filename) );

// Close file
fclose( $fp );
?>

The above code will download all kind of files if we correctly change the "Content-Type" settings in header(). Some content types are shown below :

If the MIME type starts with "vnd" ( in case of .ppt, .word or .xls files ), it means to be vendor specific. If the type starts with "x-", it means that it is non-standard, ( not registered with the "Internet Assigned Numbers Authority" ).

Images :: 
.png : header("Content-Type:image/png") 
.jpg : header("Content-Type:image/jpg") 
.gif : header("Content-Type:image/gif") 
.bmp : header("Content-Type:image/bmp") 

Texts ::
.txt : header("Content-Type:text/plain") 
.html : header("Content-Type:text/plain") 
.php : header("Content-Type:text/plain") 
.js : header("Content-Type:text/plain") or header("Content-Type:text/javascript") (obsolete) or header("Content-Type:application/javascript")
.xml : header("Content-Type:text/xml")
.csv : header("Content-Type:text/csv")
.css : header("Content-Type:text/css")

Video ::
.mpg : header("Content-Type:video/mpeg")
.mp4 : header("Content-Type:video/mp4")
.3gp : header("Content-Type:video/3gpp")

Audio ::
.aac : header("Content-Type:audio/x-aac")
.mp3 : header("Content-Type:audio/mpeg")

Zip ::
.7z : header("Content-Type:application/x-7z-compressed")
.bz : header("Content-Type:application/x-bzip")
.zip : header("Content-Type:application/zip")

Other ::
.pdf : header("Content-Type:application/pdf")
.xls : header("Content-Type:application/vnd.ms-excel; charset=utf-8")
.ppt : header("Content-Type:vnd.ms-powerpoint")
.doc : header("Content-Type:application/vnd.openxmlformats-officedocument.wordprocessingml.document")
.swf : header("Content-Type:application/x-shockwave-flash")
.bin/.exe : header("Content-Type:application/octet-stream")

Thursday, February 13, 2014

Text File Reading in PHP

A text file can be read in many ways, however reading PDFs or Excel files will be different because there format is complex. We usually do not need to read complex file types, so, we'll mostly stick to reading text files. 

Usually text files can be read -- i) all at once ii) character by character iii) line by line iv) in bytes. Check the code below.

First we would read the total content at once from a file. 

<?php
// Method 1
// This prints the text in original format
// file_get_contents() read the whole content 
// into a string variable $str
echo $str = nl2br(file_get_contents("test.txt"));

// Method 2 :: readfile() writes to output buffer.

// If output buffering is turned off, content won't 
// be displayed
readfile("test.txt");

// If we clean the buffer, the content is 

// not displayed on browser
readfile("test.txt");
ob_end_clean();

// Let's get all the buffered content into a string

// ob_get_clean() returns the buffer content as a string
// This again prints in content in original format
readfile("test.txt");
$str = ob_get_clean();
echo nl2br( $str );

/// Method 3 :: Grab the total content using fread()

// Open the file
$file_name = "test.txt";
$fp = fopen($file_name,"r+") or die("File can't be opened");
// GEt the total contents
echo nl2br(fread($fp, filesize($file_name) ));
fclose($fp);
?>

Now, let's try to read the character by character. Check the code below.

<?php
// Open the file
$file_name = "test.txt";
$fp = fopen($file_name,"r+") or die("File can't be opened");

// Loop every character

while( ($char = fgetc($fp))!== false )

  if( ord($char) == 13 )  
    echo "<br>";
  else
    echo $char;
}

// Close the file handle/pointer
fclose($fp);
?>

The ord() function returns the ASCII code of passed character. So, for every newline character, we are printing a <br> element to keep the original formatting.  

Next, we would be reading a file line by line using fgets() function. Check the code below.

<?php
// Open the file
$file_name = "test.txt";
$fp = fopen($file_name,"r+") or die("File can't be opened");

// Loop thru lines

while( ($str = fgets($fp))!= NULL )
 echo $str . "<br>";

// Close the file handle/pointer

fclose($fp);
?>

The above code reads all the lines one by one from beginning. To read a file line by line from the end, check the article How to read a file backward in PHP?.

Next, we would read a file byte by byte which is again done with fread().

<?php
$file_name = "test.txt";
$fp = fopen($file_name,"r+") or die("File can't be opened");

// Loop thru bytes
while ( ( $data = fread($fp, 10) )!= false )
 echo nl2br($data);

// Close file 
fclose($fp);
?>

The above code reads 10 bytes at a time including the newline character. Let's twist the output of the above program and discover some good stuffs.

Let's change the while loop as shown below :

<?php
// Loop thru bytes
while ( ( $data = fread($fp, 10) )!= false )
{
  $data = str_replace("\n","@",$data);
  $data = str_replace("\r","#",$data);
  echo "[$data]<br>";
}
?>

Suppose the file test.txt has the following content ::

Line 1 :: THIS IS JUST a TEST
Line 2 :: THIS IS JUST a TEST
Line 3 :: THIS IS JUST a TEST

and if the above code is run on Xampp or Wamp on Windows, the above file content would be perceived as following ::

Line 1 :: THIS IS JUST a TEST\r\n
Line 2 :: THIS IS JUST a TEST\r\n
Line 3 :: THIS IS JUST a TEST\r\n

Newline character on Windows is comprised of '\r' and '\n'. The output of the above code will prove that. The output is shown below :

[Line 1 :: ]
[THIS IS JU]
[ST a TEST#]
[@Line 2 ::]
[ THIS IS J]
[UST a TEST]
[#@Line 3 :]
[: THIS IS ]
[JUST a TES]
[T#@]


The above output proves i) fread() reads through newline character and ii) Newline character on Windows is '\r\n'.

The $bytes_pos holds a value 0 (zero) initially and file pointer is standing at 0th location. Then fread() function reads 10 bytes, hence the file pointer points to 10th location now which is returned by ftell(). So, this way the $bytes_pos array keeps on storing positions like 0, 10, 20, 30 40. When it comes to the reading last 10 bytes, ftell() still returns the position of current bytes (which can be EOF or a newline) within file which is stored at the last position in the array $bytes_pos. This would cause a small problem in our next example. 

Let's try a useless but a different thing with fread(). Let's read all these data packs (10 bytes each) backward. Check the code below.

<?php
$read_length = 10;

$file_name = "tester.txt";
$fp = fopen($file_name,"r+") or die("File can't be opened");

// Store the First Position 0, file reading starts from
// Position 0 within file
$bytes_pos = array(0);

// Loop thru bytes and store bytes position in an array
while ( ( $data = fread($fp, $read_length) )!= false )
 $bytes_pos[] = ftell( $fp );

// Reverse the array for reading it Backward 
$bytes_pos = array_reverse( $bytes_pos );

// Finally, reading 10 bytes from stored positions
foreach($bytes_pos as $pos)
{
  // Move the file pointer
  fseek( $fp, $pos );
  
  // Read
  $data = fread($fp, $read_length) ;
  $data = str_replace("\n","@",$data);
  $data = str_replace("\r","#",$data);
  echo "[$data]<br>";
}

// Close file 
fclose($fp);
?>

Here again, we started with reading 10 bytes and storing the position in an array $bytes_pos. The ftell() function returns the current position of file pointer. When the array is filled with various positions 10 bytes apart, we just reversed it. Then finally we iterated through the array using the foreach loop construct. Next, we used the fseek() function to move the file pointer to desired location and read 10 bytes from thereon using fread() function. Check the output below. It is almost the opposite to the previous output.

 [] [T#@]
[JUST a TES]
[: THIS IS ]
[#@Line 3 :]
[UST a TEST]
[ THIS IS J]
[@Line 2 ::]
[ST a TEST#]
[THIS IS JU]
[Line 1 :: ]

The first line "[]" is coming because of the fact that file position of EOF or newline is stored at the last location within the array. This was discussed awhile ago. However this problem can be overcome by removing the last item from that array.