Monday, July 10, 2017

Adding a Simple product to Cart in Magento externally

In this article, we'll see how we can insert a simple product to Magento Cart from an external PHP script.

We are testing this on Magento 1.9.2.2 and we have a simple product with ID 375 which we'll push to Cart. 

Let's see the code below. First, we are showing the existing Cart, then we'll insert a simple product ID 375, and finally we'll print the new Cart.

<?php
// Load Magento 
require_once("app/Mage.php");
umask(0);
Mage::app();

/// We Build a String for printing
$str = "";
$str .= "1. Showing existing Cart<br>===============<br>";

/// This line connects to the FrontEnd session
Mage::getSingleton('core/session', array('name' => 'frontend'));  

// Get customer session
$session = Mage::getSingleton('customer/session'); 

// Get cart instance
$cart = Mage::getSingleton('checkout/cart'); 
$cart->init();

// Get CART Count
$cartItemsCount = $cart->getItemsCount();
$str .= "Count >> $cartItemsCount <br>";

// GET CART Items
$quote = $cart->getQuote();
$cartItems = $quote->getAllVisibleItems();

// Loop Through ITEMs
foreach ($cartItems as $item) 
{
    $productId = $item->getProductId();
    $product = Mage::getModel('catalog/product')->load($productId);
$str .= "Id: $productId, Price: " . $item->getPrice() 
                . ", Qty:" . $item->getQty() . "<br>";
    // Do something more
}

// Build String
$str .= "<br><br>2. Adding a New Product<br>===============<br>";

// SET Product ID
$productId = 375;

// Build Product Params
$productInstance = Mage::getModel('catalog/product')->load($productId);
$param = array(
'product' => $productInstance->getId(),
'qty' => 1
);

// Finally ADD the product to CART
$cart->addProduct($productInstance, $param);            
// Save The Cart
$cart->save(); 

// Update Session
$session->setCartWasUpdated(true);

// Building the String
$str .= "<br><br>3. Showing the new Cart<br>===============<br>";

// Get cart instance
$cart = Mage::getSingleton('checkout/cart'); 
$cart->init();

// Get CART Count
$cartItemsCount = $cart->getItemsCount();
$str .= "Count >> $cartItemsCount <br>";

// GET CART Items
$quote = $cart->getQuote();
$cartItems = $quote->getAllVisibleItems();

// Loop Through ITEMs
foreach ($cartItems as $item) 
{
    $productId = $item->getProductId();
    $product = Mage::getModel('catalog/product')->load($productId);
    $str .= "Id: $productId, Price: " . $item->getPrice() 
            . ", Qty:" . $item->getQty() . "<br>";
    // Do something more
}

// Print the String
echo $str;
?>

The above code is quite self explanatory. We tried to connect to frontend customer's session and get his Cart details. If customer is not logged in, then it does not create any problem.

See some screenshot here : 

Step 1. Existing Cart


Step 2. Our Script Runs



Step 3. Final Cart




In this example, we added a simple product. To add a configurable product to Cart, we need to do some extra work which I'll show in next article.

Hope this helps.

Saturday, June 24, 2017

Running Laravel on Windows

Here I'll discuss about how we can install and run Laravel on Windows environment and start development in Laravel. 

First step is to download the Laravel package from https://github.com/laravel/laravel and Unzip it in a folder, say C:\xampp2\htdocs\laravel. Notice that my Xampp is installed in c:\xampp2.

Now we have two ways we can start the Laravel project running.

First Method :: From Command Line

A. First, create a DB through phpmyadmin, the name I gave is 'homestead'

B. Secondly, rename the file .env.example to .env file which holds all the basic configuration for running Laravel. It contains the DB configuration, application URL. We need to make changes accordingly. 
  
See that APP_KEY= is still left blank. We'll generate a key for our application shortly.

C. In this situation, if we try yo run Laravel server by issuing command "php artisan serve", it will show some "file not found" error as some dependencies are still unavailable.  



  
  We need to install them by issuing "composer install". 
  

  
D. After the command "composer install" ran, we can see that a new folder called "vendor" is created where various libraries are installed. 

E. Now, see what happens if we try to run the server by issuing "php artisan serve

    The server is run. Below message is shown in the command line .. 
   
    Laravel Development Server started: <http://127.0.0.1:8000>
   
   and if we hit the URL http://127.0.0.1:8000 in browser, see what happens
   
  


   
So, we need to follow some more steps as shown below.

F. Press ctrl + c to stop the server. Run command "php artisan key:generate" to create new Security Key.

   If the command line console shows any message like "Dotenv values containing spaces must be surrounded by quotes.", it means any settings provided in .env file need to be wrapped by a quote (") or (').
   
   

   
   See, it is reported that an Application key has been generated. The environment configuration file .env is updated also with the new key. In my case, it is as shown below :
   
 APP_KEY=base64:6Ed0zZN/3u+Q58OyZn6JnTvXjO7/vxM0Jdvder18QU8=
   
G. Now issue "php artisan serve" to start your server. Visit the webpage at http://127.0.0.1:8000.

Second Method :: The other way

A. The steps till Security Key Generation shown above are still required here. But we don't need to issue the command 'php artisan serve' here ...

Start your Xampp. In my case, I have a configuration where port 80 is listened by IIS, 8082 is listened by Xampp Apache. 

I have Xampp 3.2.2 installed on my Windows 8. In my case, Xampp is installed in c:\xampp2 folder.

Here, I'll be assigned the Laravel to a different port 8060.  

So, I just create a Virtual Host in file c:\xampp2\apache\conf\extra\httpd-vhosts.conf file as shown below.

<VirtualHost *:8060>
    DocumentRoot "C:\xampp2\htdocs\laravel\public"
    ServerName laravel.dev
</VirtualHost>

So, my Xampp will listen/serve to 8060 port as well.

If I change this 8060 to default 8082, my Xampp Home page (http://127.0.0.1:8082) will be replaced by Laravel, which I don't want.

B. Next, start the Xampp server. See below, the port numbers apache is serving now.




C. Hit "http://127.0.0.1:8060/" in the browser URL to see Laravel running.

D. You can also modify c:\windows\system32\drivers\etc\hosts file to have entry like this :
   
  127.0.0.1:8060  laravel.dev
   
   So, if you enter 'laravel.dev' in address bar of the browser, it will point to 127.0.0.1:8060.

The 'public' directory inside the "Laravel" installation contains the index.php file, which is the entry point for all requests to our  application. This directory also holds all JavaScript, images and CSS.

I hope this helps.

Friday, August 12, 2016

How to Show All Magento Products Externally in PHP

In our previous article, we have seen how to display all the Magento orders externally (Not within Magento itself) in php.

Now, let's see a piece of code which applies same logic and show all products. Let's check out the code.

<?php
require_once 'app/Mage.php';
umask(0);
Mage::app();
// SEARCH MODE
if($_GET['q'] && trim($_GET['q']) !="")
{
    $collection =                                                                Mage::getResourceModel('catalog/product_collection')
             ->addAttributeToSelect('*')
            ->addAttributeToFilter( 
              array(
array('attribute'=> 'name',
      'like' => '%'.trim($_GET['q']).'%'
                     ),
array('attribute'=> 'sku',
                   'like' => '%'.trim($_GET['q']).'%'
                     ),
    ))
             ->load();
}
else
/// Normal Procedure
{
   $collection = Mage::getModel('catalog/product')
                ->getCollection()
                ->addAttributeToSelect('*')
->addAttributeToSort('name', 'ASC')
->load();
}
?>

The above code includes the Search handling part also. The condition if($_GET['q'] && trim($_GET['q']) !="") evaluates to true when user enters some keyword and hits Search button.

->addAttributeToFilter( array(
array('attribute'=> 'name',
      'like' => '%'.trim($_GET['q']).'%'
                      ),
array('attribute'=> 'sku',
      'like' => '%'.trim($_GET['q']).'%'
                      ),
))

Here, the addAttributeToFilter() generates an SQL like this

WHERE 'name' LIKE '%search%' OR 'sku' LIKE '%search%'

Next, we just need to iterate through the product collection $collection.

<table width="100%" border="0">
  <tr>
  <td colspan="5" align="center">
   <div>
    <form method="get">
      <input type="text" value="<?php echo $_GET['q'];?>" name="q">
      <button title="Search" type="submit">Search</button>
    </form>
   </div>
  </td>
  </tr>
  <tr class="header">
    <th width=""><strong>ID</strong></th>
    <th width=""><strong>Name</strong></th>
    <th width=""><strong>SKU</strong></th>
    <th width=""><strong>Price</strong></th>
    <th width=""><strong>Final Price</strong></th>
  </tr>
 <?php 
  // LOOP the Collection
  foreach ($collection as $_product)
  {
    $productID = $_product->getId();
    $_product  = Mage::getModel('catalog/product')
                  ->load($productID);
    $productPosition = 0;

    // Product is Enabled and VISIBILITY = Search, Catalog 
    if($_product->getStatus()==1 && $_product->getVisibility()==4)
    {
       // GET FINAL PRICE aftyer applying RULE
       $finalPrice = Mage::getModel('catalogrule/rule')
                     ->calcProductPriceRule( $_product, 
                      $_product->getPrice() );
       $_specialPrice = $_product->getFinalPrice();
       if($_specialPrice<=$finalPrice)
       {
 $finalPrice = $_specialPrice;
       }
       if($finalPrice)
       {
 $finalPrice = Mage::helper('core')->currency( 
                           $finalPrice, true, false);
       }
       else
       {
  $finalPrice = Mage::helper('core')->currency(
                 $_product->getFinalPrice(), true, false);
       }

       echo "<tr>";
       echo "<td>".$productID."</td>";
       echo "<td><a href='" . 
             $_product->getProductUrl() .
            "'>" . $_product->getName() .                                            "</a></td>";
       echo "<td>".$_product->getSku()."</td>";
       echo "<td>".Mage::helper('core')->currency( 
                 $_product->getPrice(), true, false).                                    "</td>";
       echo "<td>".$finalPrice."</td>";
       echo "</tr>";

    }
 }
?>
</table>

We are showing all the products which have Visibility 4 (i.e "Catalog, Search"). Also the product's final price is the price we get after applying any Rules.

Check the screenshot below.



Hope this helps.

How to Show All Magento Orders Externally in PHP

The main objective is to show all the orders on a separate PHP page which won't be part of Magento.

So, we create a PHP script salesorders.php and put it in the root folder "public_html". We would use Magento's functionality by including its core files.

Now, let's check how we should start ...

<?php
error_reporting(1);

// Here We load Mage class
require_once 'app/Mage.php';

// BootStrap the Magento
Mage::app();

// GET ALL ORDERS
$orders = Mage::getResourceModel('sales/order_collection')
          ->addAttributeToSelect('*')
          ->addFieldToFilter('status', 
         array("in" => array('complete', 'closed'))
    )
          ->addAttributeToFilter('store_id', 
                  Mage::app()->getStore()->getId())
          ->addAttributeToSort('created_at', 'desc')
          ->load();
?>

In the above code, we are selecting all the Orders which have status either 'complete' or 'closed'. Also, we are fetching the Orders with the 'created_at' field in descending order.

Then we just need to iterate through the collection and generate an HTML. 

<?php 
foreach($orders as $order)
{
  /// GRAND TOTAL
  $grand_total = $order -> grand_total;

  /// CUSTOMER DETAILS
  $customer_email = $order -> customer_email;
  $customer_fname = $order -> customer_firstname;
  $customer_lname = $order -> customer_lastname;

  /// IF Customer names are blanks
  if($customer_fname == '' )
  {
   $billing_address_data = $order->getBillingAddress()->getData();
   $customer_fname = $billing_address_data['firstname'];
  }
  if($customer_lname == '' )
  {
    $billing_address_data = $order->getBillingAddress()->getData();
    $customer_lname = $billing_address_data['lastname'];
  }

  /// ORDER ID
  $increment_id = $order -> increment_id;
  $created_at   = $order -> created_at;

  $str  = "<tr>";
  $str .= "<td>$customer_fname $customer_lname <i>                              ($customer_email)</i></td>";
  $str .= "<td><b>$increment_id</b> Created on                              $created_at</td>";
  $str .= "<td>";

  /// GET all Visible Products
  /// purchased in the ORDER
  $items = $order->getAllVisibleItems();
  $largeItems = 0;

  /// LOOP thru ITEMs
  foreach($items as $i)
  {
    /// PRODUCT DETAILS
    $prod_id = $i->getProductId();
    $p = Mage::getModel('catalog/product')->load($prod_id);
    $sku = $p->getSku();
  
    /// Build HTML
    $str .=  "<a href='" . $p->getUrlPath() . "'>" . $i->getName() . "</a>";
  
    /// PRODUCT Options
    $prodOptions = $i->getProductOptions();
    /// LOOP thru product Options and Show Them
    foreach ( $prodOptions['attributes_info'] as $key => $val)
    {
$str .= "[" . $val['label'] . ":"; 
       $str .= $val['value'] . "] ";
    } 
  }

  $str .= "</td>";
  $str .= "</tr>";

  /// PRINT HTML
  echo $str ;
}
?>

Now check the Output we get.




Now, we can add a Search facility to our script; it will search the POSTed word with Customer name or email within the order. So, we need a <form> tag in our HTML. 

<form method="get" action="" id="search_mini_form">
    <input type="text" name="q" placeholder="Search Customer">
    <input  title="Search" type="submit" value="Search">
</form>

So, through PHP we need to receive this POSTed value and add some filtering code to the Order collection. The code is shown below. 

<?php
/// SEARCH 
if($_GET['q'] && trim($_GET['q']) != "")
{
  /// LIKE Query
  $likeStr = '%'.trim($_GET['q']).'%';

  $orders = Mage::getResourceModel('sales/order_collection')
      ->addAttributeToSelect('*')
      ->addFieldToFilter('status', array("in" => array(
           'complete', 'closed')
        ))
      ->addAttributeToFilter( 'store_id', 
               Mage::app()->getStore()->getId())
      ->addFieldToFilter( 
             array( 'customer_email', 'customer_firstname', 'customer_lastname'),
     array( array( 'like'=>$likeStr ), 
            array( 'like'=>$likeStr ), 
    array( 'like'=>$likeStr ) 
  )
)
      ->addAttributeToSort('created_at', 'desc')
      ->load();
    
  /// IF u want to show SQL
  /// uncomment below line
  /// echo $orders->getSelect()->__toString();
}
else
/// FOR Non-Search 
{
   $orders = Mage::getResourceModel('sales/order_collection')
       ->addAttributeToSelect('*')
       ->addFieldToFilter('status', array("in" => array(
            'complete', 'closed')
          ))
       ->addAttributeToFilter('store_id', 
            Mage::app()->getStore()->getId())
       ->addAttributeToSort('created_at', 'desc')
       ->load();

?>

If it is a search, then if($_GET['q'] && trim($_GET['q']) != "") is true and the first part of the if-else structure is executed. 

See, how we have captured the submitted value in variable "$likeStr" and used "addFieldToFilter" function for filtering the collection.

IF we need "AND" conditions, then we can use multiple addFieldToFilter() calls.

To use "OR", we need to modify the addFieldToFilter() as shown below.

->addFieldToFilter( 
array( 'customer_email', 
               'customer_firstname', 
               'customer_lastname'),
array( array( 'like'=>$likeStr ), 
      array( 'like'=>$likeStr ), 
       array( 'like'=>$likeStr ) 
     )
  )

above statement generates the following SQL

'customer_email' like $likeStr OR 'customer_firstname' like $likeStr OR 'customer_lastname' like $likeStr

To generate an "AND" SQL query like this :: 
'customer_email' like $likeStr AND 'customer_firstname' like $likeStr, 

we can use the following structure ::

->addFieldToFilter('customer_email',  array("like" => $likeStr))
->addFieldToFilter('customer_firstname',array('like'=>$likeStr))

You can download the full working code here.

In our next tutorial, we'll list all the products available in Magento from an external script.

Monday, July 04, 2016

How to add Custom Fields to Contact Form in Magento

Magento comes with a nice Contact Us form which helps customers to keep in touch with store owners. But in this article we'll see how we can easily add more fields to this Magento Contact form.

Let's check out the various settings in the Admin Panel first.

Below is the Store Email Address section. Any one of them can be used with the Contact Forms Email sender. We'll use "Customer Support" as our sender.



Next, let's take a look at Contact Form settings.



Here, we have the Target email address where the Contact Email will be sent to. Email template can also be selected here.

So, now we need to make changes to 3 more files... 

1. app/design/frontend/default/YOUR_THEME/template/contacts/form.phtml
2. app/code/core/Mage/Contacts/controllers/IndexController.php
3. app/locale/en_US/template/email/contact_form.html

The first file is the contact form template where we would include our new fields.
The IndexController.php is the controller where we can add server side validation to the field.
The third file is the Email Template.

Let's start with the Contact Form template. We are going to add a "Subject" field to our form. So, we just create an <input> element with a name "subject". If we want to make it a required entry, let's add "required-entry" to its class. This will add JavaScript validation to it. Check out the HTML below.

<li>
  <div class="input-box">
    <input name="subject" id="subject" title="Subject" value="" class="input-text required-entry" type="text" placeholder="Subject" />
  </div>
</li>

Now, as it is a required field, we need to add a server side validation as well. Here the controller IndexController.php comes in help. We can add our validation within postAction() method as shown below. Here we have checked if the value is an empty one.

if (!Zend_Validate::is(trim($post['subject']) , 'NotEmpty')) 
{
    $error = true;
}

If Server side validation fails, an error message "Unable to submit your request. Please, try again later" is shown.

Now, let's modify the Email template to include the new field "Subject". Open the email template file app/locale/en_US/template/email/contact_form.html. 



See how we have included the field "subject" in our email template by writing ::

Subject: {{var data.subject}}

Another thing, the first line of this template says "<!--@subject Our CUSTOM Contact Form@-->". This will be the Contact Email Subject line. Here I have modified it to "Our CUSTOM Contact Form".

Final result is, one email with subject line "Our CUSTOM Contact Form" is sent from "Customer Support" profile to "storeowner@store.com" as entered in "System > Configuration > Contacts" shown above.

This way we can add any number of fields to the Contact Form, add validations to it, and include them in the final email.

Thursday, June 30, 2016

Parsing Complex XML with SimpleXML in PHP

Let's parse a very complex XML Data using SimpleXML methods in PHP. Let's take a complex XML data as shown below.

<NodeLevel1>
 <NodeLevel2>
  <NodeLevel3>
<RaceDay RaceDayDate="2016-06-29" >
   <Meeting MeetingCode="BR" MtgId="1299709952" VenueName="Doomben" >
<Pool PoolType="DD" DisplayStatus="SELLING"></Pool>
<Pool PoolType="XD" DisplayStatus="PAYING"></Pool>
<Pool PoolType="TT" DisplayStatus="CLOSED"></Pool>
<Pool PoolType="QD" DisplayStatus="CLOSED"></Pool>
<MultiPool PoolType="XD" DisplayStatus="PAYING"></MultiPool>
<Race RaceNo="1" RaceTime="12:53" RaceName="2YO HANDICAP" />
<Race RaceNo="2" RaceTime="13:23" RaceName="BM 75 HANDICAP" />
<Race RaceNo="3" RaceTime="13:53" RaceName="MAIDEN PLATE" >
     <TipsterTip TipsterId="0" Tips="4"/>
     <TipsterTip TipsterId="5" Tips="1-9-4-8"/>
     <Pool PoolType="A2" Available="Y" Abandoned="N" />
     <Pool PoolType="EX" Available="Y" Abandoned="N" />
     <Pool PoolType="F4" Available="Y" Abandoned="N" />
     <Runner RunnerNo="1" RunnerName="ALL TROOPS" />
     <Runner RunnerNo="2" RunnerName="SEQ THE STAR" />
     <Runner RunnerNo="3" RunnerName="SHADOW LAWN"/>
     <Runner RunnerNo="4" RunnerName="FREQUENDLY" />
</Race>
<Tipster TipsterId="0" TipsterName="LATE MAIL"/>
<Tipster TipsterId="1" TipsterName="RADIO TAB"/>
<Tipster TipsterId="2" TipsterName="TRACKMAN"/>
 </Meeting>
</RaceDay>
<RaceDay RaceDayDate="2016-06-30" >
 <Meeting MeetingCode="MR" MtgId="2299719559" VenueName="Lucas" >
<Pool PoolType="CC" DisplayStatus="SELLING"></Pool>
<Pool PoolType="YD" DisplayStatus="PAYING"></Pool>
<Pool PoolType="VT"  DisplayStatus="CLOSED"></Pool>
<Pool PoolType="MD" DisplayStatus="CLOSED"></Pool>
<MultiPool PoolType="VD" PoolDisplayStatus="PAYING"></MultiPool>
<Race RaceNo="1" RaceTime="12:53" RaceName="R2YO BHANDI" />
<Race RaceNo="2" RaceTime="13:23" RaceName="XX 75 ZINDA" />
<Race RaceNo="3" RaceTime="13:53" RaceName="PLATE RAIDEN" >
    <TipsterTip TipsterId="0" Tips="5"/>
    <TipsterTip TipsterId="5" Tips="2-1-4-8-4"/>
    <Pool PoolType="A2" Available="Y" Abandoned="N" />
    <Pool PoolType="EX" Available="Y" Abandoned="N" />
    <Pool PoolType="F4" Available="Y" Abandoned="N" />
    <Runner RunnerNo="1" RunnerName="ALL BROOKS" />
    <Runner RunnerNo="2" RunnerName="MIDDLE STAR" />
    <Runner RunnerNo="3" RunnerName="LONELY LAWN"/>
    <Runner RunnerNo="4" RunnerName="OBLIV" />
</Race>
<Tipster TipsterId="0" TipsterName="EARLY MAIL"/>
<Tipster TipsterId="1" TipsterName="RADIO CAB"/>
<Tipster TipsterId="2" TipsterName="JACKMAN"/>
 </Meeting>
  </RaceDay>
 </NodeLevel3>
</NodeLevel2>
</NodeLevel1>  

See that <NodeLevel3> node has two <RaceDay> nodes in it. And each <RaceDay> has its own <Meeting> node. Again each <Meeting> node  has various nodes like <Pool>, <MultiPool>, <Race>, <Tipster> as its children. Finally each <Race> node has <TipsterTip>, <Pool> and <Runner> nodes under it.

Here, Most of the nodes have attributes and some have descendants under it. We would traverse through all the <RaceDay> nodes and finds all its attributes and children. Let's start it.

<?php
$xml_source = <<<EOD
<NodeLevel1>
 <NodeLevel2>
  <NodeLevel3>
   <RaceDay RaceDayDate="2016-06-29" >
     <Meeting MeetingCode="BR" MtgId="1299709952" VenueName="Doomben" >
      <Pool PoolType="DD" DisplayStatus="SELLING"></Pool>
      <Pool PoolType="XD" DisplayStatus="PAYING"></Pool>
      <Pool PoolType="TT" DisplayStatus="CLOSED"></Pool>
      <Pool PoolType="QD" DisplayStatus="CLOSED"></Pool>
      <MultiPool PoolType="XD" DisplayStatus="PAYING">
      </MultiPool>
      <Race RaceNo="1" RaceTime="12:53" RaceName="2YO HANDICAP"/>
      <Race RaceNo="2" RaceTime="13:23" RaceName="BM7 HANDICAP"/>
      <Race RaceNo="3" RaceTime="13:53" RaceName="MAIDEN PLATE">
       <TipsterTip TipsterId="0" Tips="4"/>
       <TipsterTip TipsterId="5" Tips="1-9-4-8"/>
       <Pool PoolType="A2" Available="Y" Abandoned="N" />
       <Pool PoolType="EX" Available="Y" Abandoned="N" />
       <Pool PoolType="F4" Available="Y" Abandoned="N" />
       <Runner RunnerNo="1" RunnerName="ALL TROOPS" />
       <Runner RunnerNo="2" RunnerName="SEQ THE STAR" />
       <Runner RunnerNo="3" RunnerName="SHADOW LAWN"/>
       <Runner RunnerNo="4" RunnerName="FREQUENDLY" />
     </Race>
     <Tipster TipsterId="0" TipsterName="LATE MAIL"/>
     <Tipster TipsterId="1" TipsterName="RADIO TAB"/>
     <Tipster TipsterId="2" TipsterName="TRACKMAN"/>
   </Meeting>
  </RaceDay>
  <RaceDay RaceDayDate="2016-06-30" >
   <Meeting MeetingCode="MR" MtgId="2299719559" VenueName="Las" >
    <Pool PoolType="CC" PoolDisplayStatus="SELLING"></Pool>
    <Pool PoolType="YD" PoolDisplayStatus="PAYING"></Pool>
    <Pool PoolType="VT" PoolDisplayStatus="CLOSED"></Pool>
    <Pool PoolType="MD" PoolDisplayStatus="CLOSED"></Pool>
    <MultiPool PoolType="VD" DisplayStatus="PAYING">
    </MultiPool>
    <Race RaceNo="1" RaceTime="12:53" RaceName="R2YO BHANDI" />
    <Race RaceNo="2" RaceTime="13:23" RaceName="XX 75 ZINDA" />
    <Race RaceNo="3" RaceTime="13:53" RaceName="PLATE RAIDEN" >
     <TipsterTip TipsterId="0" Tips="5"/>
     <TipsterTip TipsterId="5" Tips="2-1-4-8-4"/>
     <Pool PoolType="A2" Available="Y" Abandoned="N" />
     <Pool PoolType="EX" Available="Y" Abandoned="N" />
     <Pool PoolType="F4" Available="Y" Abandoned="N" />
     <Runner RunnerNo="1" RunnerName="ALL BROOKS" />
     <Runner RunnerNo="2" RunnerName="MIDDLE STAR" />
     <Runner RunnerNo="3" RunnerName="LONELY LAWN"/>
     <Runner RunnerNo="4" RunnerName="OBLIV" />
    </Race>
    <Tipster TipsterId="0" TipsterName="EARLY MAIL"/>
    <Tipster TipsterId="1" TipsterName="RADIO CAB"/>
    <Tipster TipsterId="2" TipsterName="JACKMAN"/>
  </Meeting>
 </RaceDay>
</NodeLevel3>
</NodeLevel2>
</NodeLevel1>
EOD;
?>

See how I have declared the XML in a string using Heredoc in PHP.

$xml_source = <<<EOD

When using Heredoc, we need to make sure that there is no blankspace after the opening identifier. So, "<<<EOD" must be followed by a newline "\n"; which means in the editor, after typing "<<<EOD" we need to press ENTER to move to the new line.

Heredoc helps us to avoid quote (' or ") usage problems. See, all the node attributes are wrapped in double quote. We have another method to define the XML string as shown below. 

/// We make sure that all single quotes are escaped
$xml_source = '<NodeLevel1><NodeLevel2><NodeLevel3>' .
              '<RaceDay Name="John O\'Neal" > ..... '; 
 
Ok, now let's proceed.

// LOAD the XML Root Object
$all_nodes = new SimpleXMLElement($xml_source);

// BROWSE to Certain PATH/NODE
$all_nodelevel3 = $all_nodes
                  ->xpath('/NodeLevel1/NodeLevel2/NodeLevel3');

// PRINT what we got
print_r($all_nodelevel3);

The above piece of code would load the XML data, create SimpleXMLElement Object with it. Then we are traversing to "/NodeLevel1/NodeLevel2/NodeLevel3" node in the XML tree. xpath method actually searches the SimpleXML node for children matching the XPATH provided as its argument. We don't add the trailing slash ('/') to the end of our XPATH. 

To get all the <NodeLevel1> we need to pass "/NodeLevel1" as argument to xpath() method.

Now, let's print all the <Runner> nodes in the above XML.

<?php
// LOAD the XML Root Object
$all_nodes = new SimpleXMLElement($xml_source);

// BROWSE to all <NodeLevel3>
$all_nodelevel3 = $all_nodes->xpath('/NodeLevel1/NodeLevel2/NodeLevel3');

// LOOP THRU <NodeLevel3> nodes
foreach($all_nodelevel3 as $nodelevel3)
{
  // GEt All <RaceDay> Nodes
  $all_racedays = $nodelevel3->RaceDay;
  
  // LOOP THRU All <RaceDay> Nodes
  foreach($all_racedays as $raceday)  
  {
// GET ALL <Meeting>
$all_meeting = $raceday->Meeting;
 
// Loop Thru <Meeting>
foreach($all_meeting as $meeting)
{
     // GET ALL RACE
     $all_race = $meeting->Race;

     // LOOP Thru <Race> Nodes
     foreach($all_race as $race)
     {
 
       // GET ALL <Runner> nodes
       $all_runners = $race->Runner;
 
       /// Note that some <Race> nodes don't have
       /// <Runner> nodes under it
       /// So, we check if <Runner> nodes exist
       if($all_runners)
       {
/// Loop Thru <Runner> nodes
foreach($all_runners as $runner)
{
          /// GEt <Runner> Node's attributes
          $atts = $runner->attributes();

          // Loop Thru Attributes
          $str = "";
          foreach($atts as $key => $val)
          {
$str .= "$key => $val, ";
          }
         // PRINT  
          echo "RUNNER  :: $str <br>";

}
       }
     }
}
 }
}
?>

Check the Output Below :: 

RUNNER :: RunnerNo => 1, RunnerName => ALL TROOPS, 
RUNNER :: RunnerNo => 2, RunnerName => SEQ THE STAR, 
RUNNER :: RunnerNo => 3, RunnerName => SHADOW LAWN, 
RUNNER :: RunnerNo => 4, RunnerName => FREQUENDLY, 
RUNNER :: RunnerNo => 1, RunnerName => ALL BROOKS, 
RUNNER :: RunnerNo => 2, RunnerName => MIDDLE STAR, 
RUNNER :: RunnerNo => 3, RunnerName => LONELY LAWN, 
RUNNER :: RunnerNo => 4, RunnerName => OBLIV, 

See, how we have used "foreach" loop structure to traverse nodes and get deeper into the XML Tree. foreach construct has been used considering that <RaceDay>, <Meeting>, <Race> and <Runner> nodes may appear in any number within their Parent Node in the XML Tree. 

We even used foreach($all_nodelevel3 as $nodelevel3) to consider that many <NodeLevel3> nodes co-exist within a single <NodeLevel2> node.

Secondly, we have used attributes() function to get all the attributes of a node.

Hope this helps.