php - How to parse formatted text that resembles an .ini file?

I have an input form in HTML, which is used to import users data into a database (MYSQL) using PHP.

The information in the input must be like this:

[account]
user = {user1}
pwd = {password1}
expdate = 2028-01-01
[account]
user = {user2}
pwd = {password2}
expdate = 2028-01-01
[account]
user = {user3}
pwd = {password3}
expdate = 2028-01-01

Now, the PHP code should be parsing the number of users and each user info and save each user info separately.

My PHP code is:

$users = $_POST["users"];
$users = explode(chr(10) . chr(10), $users);
for ($i = 0; $i < count($users); $i++) { 
    $users[$i] = explode(" ", $users[$i]);
    $username = $users[$i][2];
    $pwd = $users[$i][5];
    $exp = $users[$i][8];
    if (strtotime($exp) === false) {
        // ...code
    } else {
        $expiredate = date("Y-m-d", strtotime($exp));
    }
}

When the input is a single user:

[account]
user = {user1}
pwd = {password1}
expdate = 2028-01-01

My output is mangled (here is the var_export() output):

array (
  'username' => '{user1}
pwd',
  'pwd' => '=',
  'exp' => '2028-01-01'
)

The problem is the exploding -- the pwd key is attached to the username value, the pwd value =, and it sometimes give a wrong format for the date which prevents being saved.

As a workaround, after putting spaces after each of the input username and password, all data are parsed successfully.

Also, when I enter multiple users at the input form:

[account]
user = {user1} 
pwd = {password1} 
expdate = 2028-01-01
[account]
user = {user2}
pwd = {password2}
expdate = 2028-01-01

The PHP code only reads the first user and save the information only for the first one, I want them both being saved.

So:

  • I want the PHP code to parse all users information to be saved, not only the first user.
  • I want the user information get saved without putting space at the end of the user and password lines.

Tried to edit my code and search a lot, was unlucky , don't know what is wrong with the PHP code.

Answer

Solution:

Your input looks like an .ini file.
Use to split the input string into pieces separated by [account] then use to parse each piece and get the data in an array:

$users = <<< END
[account]
user = {user1}
pwd = {password1}
expdate = 2028-01-01
[account]
user = {user2}
pwd = {password2}
expdate = 2028-01-01
[account]
user = {user3}
pwd = {password3}
expdate = 2028-01-01
END;

// Use $users = $_POST['users'] in the real code

// Parse the input data
$data = array_map(
    function($entry) { return parse_ini_string($entry); },
    array_filter(           // remove the empty entries (before the first '[account]', between two consecutive appearances of '[account]'
        explode('[account]', $users),
        function($entry) { return strlen(trim($entry)); }
    )
);

print_r($data);

The output is:

Array
(
    [1] => Array
        (
            [user] => {user1}
            [pwd] => {password1}
            [expdate] => 2028-01-01
        )

    [2] => Array
        (
            [user] => {user2}
            [pwd] => {password2}
            [expdate] => 2028-01-01
        )

    [3] => Array
        (
            [user] => {user3}
            [pwd] => {password3}
            [expdate] => 2028-01-01
        )

)

Check it online.

Next, processing the parsed information is straight forward:

foreach ($data as $account) {
  // do something with $account['user'], $account['pwd'], $account['expdate']
  // Use the DateTime class and its friends to validate and handle $account['expdate']
}

parse_ini_string() is able to parse the entire string in one call and handle the sections correctly if its second argument is true. However, because all the sections are named account it merges the values; the key-value pairs that occur later in the input override the previous values.
This is the reason why we first split the input into sections then feed to parse_ini_string() one section at a time.

Answer

Solution:

You can try like this

PHP - HTML :
<div class="all_forms">
<?php for ($z = 0; $z < 5; $z++) { ?>
<form method="POST" id="passenger-<?= $z ?>-details">
   <input type="text" class="form-control" name="username" placeholder="Username">
   <input type="password" class="form-control" name="password" placeholder="Password">
   <input type="date" class="form-control" name="date" placeholder="date">
</form>
<?php } ?>
</div>
JavaScript - JQuery
var passenger_details_array = [];
   $('.all_forms').find("form").map(function(idx, form) {
      var str = $('#' + form.id).serialize();
      passenger_details_array.push(str);
   });
   var data = {
      token: 'SOME_SECURITY_TOKEN',
      passenger_details: passenger_details_array
   };
   $.post('http://domain/path/file.php', data, function(response) {
            console.log(response);
   });
PHP - Saving Info

$passenger_details = $_POST['passenger_details'];
foreach ($passenger_details as $value) {
   parse_str($value, $tmp);
//here you have $tmp and in this you will find all proper details like (one by one)
// ['username' => 'U1', 'password' => 'user1_pass', 'date' => 'whatever']
}

//$passenger_details has data like

[
      ['username' => 'U1', 'password' => 'user1_pass', 'date' => 'whatever'],
      ['username' => 'U2', 'password' => 'user2_pass', 'date' => 'whatever'],
      ['username' => 'U3', 'password' => 'user3_pass', 'date' => 'whatever']
]

Answer

Solution:

Split on [account] lines with optional leading newline character sequences and mandatory trailing newline character sequences. Do not allow empty elements to be generated in the explosion.

\R matches platform-agnostic newline sequences so that do have to worry about \r\n versus \n issues.

The ^ (start of string anchor) is modified by the m pattern modifier so that it matches the start of each line.

? means zero or one of whatever was before it. This makes the trailing \R optional.

If your real text has an empty line between user details, you can just add {2} after the leading \R to consume the extra newline sequence.

Call parse_ini_string() on each generated element. Clean and done; no extra filtering or trimming required. No more trouble with the exploding process.

Code: (Demo)

$data = <<<NOT_INI
[account]
user = {user1}
pwd = {password1}
expdate = 2028-01-01
[account]
user = {user2}
pwd = {password2}
expdate = 2028-01-01
[account]
user = {user3}
pwd = {password3}
expdate = 2028-01-01
NOT_INI;

var_export(
    array_map(
        'parse_ini_string',
        preg_split(
            '~\R?^\[account]\R~m',
            $data,
            0,
            PREG_SPLIT_NO_EMPTY
        )
    )
);

Output:

array (
  0 => 
  array (
    'user' => '{user1}',
    'pwd' => '{password1}',
    'expdate' => '2028-01-01',
  ),
  1 => 
  array (
    'user' => '{user2}',
    'pwd' => '{password2}',
    'expdate' => '2028-01-01',
  ),
  2 => 
  array (
    'user' => '{user3}',
    'pwd' => '{password3}',
    'expdate' => '2028-01-01',
  ),
)

Source