'PHP: How to quickly split a key=value file into associative array

When you send a PDT transaction ID back to paypal, you get back a list of transaction data. It has SUCCESS on the first line, and then a list of key=value pairs. One pair per line. For example:

SUCCESS
[email protected]
charset=windows-1252
custom=
first_name=Alice
handling_amount=0.00
invoice=NN0005
item_name=Bear
item_number=BEAR05
last_name=Foobar
mc_currency=SEK
mc_fee=13.00
mc_gross=250.00
[email protected]
payer_id=UC9DXVX7GRSTN
payer_status=unverified
payment_date=09:08:06 Oct 18, 2010 PDT
payment_fee=
payment_gross=
payment_status=Completed
payment_type=instant
protection_eligibility=Ineligible
quantity=1
receipt_id=2479-2605-1192-2880
[email protected]
receiver_id=8Y670ENTB8BY6
residence_country=NO
shipping=0.00
tax=0.00
transaction_subject=Bear
txn_id=1PH815997L239283J
txn_type=web_accept

What is a good, quick and clean way to check if the first line equals SUCCESS and then convert this into an associative array? I am able to do it, and it works, but I'm curious if there are better or cleaner ways of doing it cause what I end up with isn't always that nice. Notice that some of the keys don't have any value as well.

So, what I would like to end up with is essentially:

array(
    'business' => '[email protected]',
    'charset' => 'windows-1252',
    'custom' => NULL,
    'first_name' => Alice,

    // And so on

);

The order doesn't matter.

Update: Thanks for great suggestions! Testing them out now. Splitting the string into separate lines is also part of my problem by the way. Forgot to specify that. See that some has taken that into account and some don't. It can have an impact, since certain methods needs to first split into lines and then the pairs, while others can just eat the whole thing in one piece.

Update: I should also have mentioned that having empty ones end up as NULL would be a bonus, but probably not a requirement. They don't do that in my version either and it doesn't really matter that much.


Benchmark results

Got curious to what I should choose here, so I benchmarked how I should do different parts. Had various ideas myself, got some from here and other places as well. When I had found the fastest I could manage, I created a benchmark and put it to the test against all of the answers so far. For the ones that had skipped the splitting or the checking for success I added an explode and a strpos check accordingly. I also added urldecode to all the solutions as well, except @dynamism who tackled that so nicely. Anyways, here are the results:

Benchmark results

The benchmark was run using the codebench Kohana 3 module. Here is the benchmark code:

<?php defined('SYSPATH') or die('No direct script access.');

/**
 * Test various methods of checking for SUCCESS
 *
 * @package  PayPal
 * @category PDT
 * @author  Torleif Berger
 */
class Bench_ProcessPDT extends Codebench
{
    public $description = 'Various ways of checking that a string starts with SUCCESS';

    public $loops = 100000;


    public function bench_mine($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = urldecode(substr($subject, 7));
            preg_match_all('/^([^=]++)=(.*+)/m', $subject, $result, PREG_PATTERN_ORDER);
            $result = array_combine($result[1], $result[2]);

            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964308#3964308
    public function bench_dynamism_substr($subject)
    {
        if(substr($subject, 0, 7) == 'SUCCESS')
        {
            $subject = substr_replace($subject, '', 0, 7);
            $subject = str_replace(array("\n", "\r", "\r\n"), '&', $subject);
            parse_str($subject, $result);

            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964308#3964308
    public function bench_dynamism_strpos($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = substr_replace($subject, '', 0, 7);
            $subject = str_replace("\r\n", '&', $subject);
            parse_str($subject, $result);

            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964520#3964520
    public function bench_mellowsoon($subject)
    {
        $subject = urldecode($subject);

        $lines = explode("\r\n", $subject);
        $lines = array_map('trim', $lines);
        $status = array_shift($lines);
        if($status == 'SUCCESS')
        {
            $result = array();
            foreach($lines as $line)
            {
                list($key, $value) = explode('=', $line, 2);
                $result[$key] = $value;
            }
            return array(count($result), array_shift($result), array_shift($result));
        }

        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964265#3964265
    public function bench_amber($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = explode("\r\n", urldecode($subject));
            array_shift($subject);  // Remove is empty

            $result = array();
            foreach($subject as $line)
            {
                $bits = explode('=', $line);
                $field_name = array_shift($bits);
                $field_contents = implode('=', $bits);
                $result[$field_name] = $field_contents;
            }
            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964366#3964366
    public function bench_GigaWatt($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = explode("\r\n", urldecode($subject));

            $result = array();
            foreach($subject as $line)
            {
                if (strpos($line, "=") === FALSE)
                    continue;

                list($var, $value) = explode("=", trim($line));
                $result[$var] = empty($value) ? NULL : $value;
            }
            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964366#3964366
    public function bench_GigaWatt2($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = explode("\r\n", urldecode($subject));

            $result = array();
            foreach($subject as $line)
            {
                if (strpos($line, "=") === FALSE)
                    continue;

                list($var, $value) = explode("=", trim($line));
                $result[$var] = $value;
            }
            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }

    // http://stackoverflow.com/questions/3964219/3964323#3964323
    public function bench_dvhh($subject)
    {
        if(strpos($subject, 'SUCCESS') === 0)
        {
            $subject = explode("\r\n", urldecode($subject));

            $result = array();
            foreach($subject as $line)
            {
                $lineData = preg_split("/\s*=\s*/", $line);
                if(count($lineData) == 2)
                {
                    $result[$lineData[0]] = $lineData[1];
                }
            }
            return array(count($result), array_shift($result), array_shift($result));
        }
        return FALSE;
    }


    public $subjects = array
    (
        "SUCCESS\r\[email protected]\r\ncharset=windows-1252\r\ncustom=\r\nfirst_name=Alice\r\nhandling_amount=0.00\r\ninvoice=AF000001\r\nitem_name=Stuffed bear\r\nitem_number=BEAR05\r\nlast_name=Foobar\r\nmc_currency=USD\r\nmc_fee=2.00\r\nmc_gross=20.00\r\[email protected]\r\npayer_id=UC9DXVX7GRSTN\r\npayer_status=unverified\r\npayment_date=09:08:06 Oct 18, 2010 PDT\r\npayment_fee=\r\npayment_gross=\r\npayment_status=Completed\r\npayment_type=instant\r\nprotection_eligibility=Ineligible\r\nquantity=1\r\nreceipt_id=2479-2605-1192-2880\r\[email protected]\r\nreceiver_id=8Y670ENTB8BY6\r\nresidence_country=USD\r\nshipping=0.00\r\ntax=0.00\r\ntransaction_subject=Bear\r\ntxn_id=1PH815997L239283J\r\ntxn_type=web_accept",

        "FAIL\r\nError: 4003",

        "INVALID",
    );
}

If anyone has any further tips for improvement, please let me know :)



Solution 1:[1]

This is how I'd do it in 5 lines:

// Is this a successful post?
if( substr($paypal, 0, 7) == 'SUCCESS')
{
    $paypal = substr_replace($paypal, '', 0, 7);
    $paypal = str_replace(array("\n", "\r", "\r\n"), '&', $paypal);
    parse_str($paypal, $response_array);
}

Check to see if the postback $paypal is successful, and if it is remove the first line, replace newlines with & then run it through parse_str, sorted. Output is in $response_array ready to rock.

Same problem as Amber, though, where NULL is shown as ''.

Edit: Also, a caveat: If the array key has any special characters in it (for some strange reason, maybe a .) it is converted to an _.

Solution 2:[2]

Split off the first line separately, check it, and then use this to grab the rest:

foreach($response_lines as $line) {
    $bits = explode('=', $line, 2);
    $fields[$bits[0]] = $bits[1];
}

After which $fields will be the array you seek. (One note: items without a value in the response will have '' instead of NULL.)

Solution 3:[3]

use http://www.php.net/manual/en/function.preg-split.php

foreach($lines as $line) {
    $lineData = preg-split("\s*=\s*",$line);
    if(count($lineData)==2) {
         $result[$lineData[0]] = $lineData[1];
    }
}

Solution 4:[4]

The following will work:

foreach($response as $line) {
    if (strpos($line, "=") === FALSE) { continue; }   // Skip the line if there's no assignment action going on.
    list($var, $value) = explode("=", trim($line));   // Get the parts on either side of the "=".
    $result[$var] = (empty($value) ? NULL : $value);  // Assign the value to the result array, making the value NULL if it's empty.
}

Solution 5:[5]

Adding my 2 cents because some of the solutions posted so far are being overly clever, or not 100% correct.

$lines = explode("\n", $paypal_response);
$lines = array_map('trim', $lines);
$status = array_shift($lines);
if ($status != 'SUCCESS') {
    // Transaction was not successful
}
$values = array();
foreach($lines as $line) {
    list($key, $value) = explode('=', $line, 2);
    $values[$key] = $value;
}

Solution 6:[6]

Here is a more complex solution:

//$result=the result from paypal
parse_str(str_replace(PHP_EOL,'&',$result),$result);
var_dump($result);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Jack G
Solution 3 dvhh
Solution 4 Mr. Llama
Solution 5 mellowsoon
Solution 6 Itay Moav -Malimovka