2
\$\begingroup\$

I was asked to write PHP code that can be used to parse a config file on the same server (same directory as well) and output all configuration parameters, and have them all be cast to the correct type. I wasn't allowed to use any of the existing functions made specifically to parse files, all string manipulation.

Class File:

    <?php

interface ConfigParamType{
public function determineType($value);
}

class ParseConfig implements ConfigParamType {
    private $fileContents;  //For storing raw file contents
    private $resultArray = array(); //For storing raw configuration settings without comments
    private $parsedArray = array(); //For storing final parsed config file.
    /*
     * parseFile
     *
     * Opens the config file and will store all of the configuration parameters in an associative array
     *
     * @param (mixed) the path to the config file.
     * @return (array) returns an associative array with config params as key/value pairs.
     */
    public function parseFile($path){
        $this->fileContents = file_get_contents($path); //get config.ini contents
        $this->resultArray = explode("\n", $this->fileContents);    //explode fileContents string into array
        /*
        Iterate through the array and 
        only save array elements which contain 
        an assignment operator. Comments and white space can be ignored. 
        */
        foreach($this->resultArray as $item){   

            if(strpos($item, '=')){
                $this->parsedArray[trim(strstr($item, '=', true))] = trim(substr(strstr($item, '='), 1));
            }
        }

        foreach ($this->parsedArray as $key => $value) {
            $this->parsedArray[$key] = $this->determineType($value);
        }
        return $this->parsedArray;
    }
    /*
     * determineType
     *
     * Will determine the type of passed argument and return the value after casting to the determined type
     *
     * @param (mixed) The value whose type is to be determined.
     * @return (mixed) returns the same value but cast to the determined type.
     */
    public function determineType($value){
        if (is_numeric($value) && strpos($value, '.')) {
            return (double)$value;
        } elseif(is_numeric($value)) {
            return (int)$value;
        }elseif(stristr($value,"true") || stristr($value,"yes") || stristr($value,"on")){

            return (bool)true;
        }elseif(stristr($value, "false") || stristr($value, "no") || stristr($value,"off")){

            return (bool)false;
        }else{
            return (string)$value;
        }
    }
}
    ?>

Driver File:

<?php

require("ParseConfig.php");
define("CONFIG_FILE", "config.ini");    //Extension can be changed to anything.
$parser = new ParseConfig();
$results = array();
$results = $parser->parseFile(CONFIG_FILE); //Instantiate an object of the parser class we created and call our parseFile method.

//------------------------------------------------------------------------------------------------------
//We can output our values using foreach, but FALSE boolean values will display nothing.
echo "<h2>Outputing config file parse result values using basic foreach: </h2><br/>";
echo "<h3>FALSE boolean variables will display nothing: </h3><br/>";
foreach ($results as $configParam => $configValue) {    
    echo $configParam . " is a(n) " . gettype($configValue) . " with the value of " . $configValue . "<br/>";
}
echo "<hr>";
//----------------------------------------------------------------------------------------------------------
//To avoid this we can add a little bit of logic and use variable variables.

foreach ($results as $configParam => $configValue) {
    /*This code creates variables for us 
    that use the config file parameter names as the variable identifier, 
    and contain the parameter values. (variable variables)
    */
    $$configParam = $results[$configParam];

}
//All of these variables were dynamically created.
echo "<h2>Outputing config file parse result values using variable variables and logic to properly display boolean types. : </h2><br/>";
echo "host: {$host} and its Data Type is ". gettype($host) . "<br/>";
echo "server_id: {$server_id} and its Data Type is ". gettype($server_id) . "<br/>";
echo "server_load_alarm: {$server_load_alarm} and its Data Type is ". gettype($server_load_alarm) . "<br/>";
echo "user: {$user} and its Data Type is ". gettype($user) . "<br/>";
if ($verbose) {
    echo "verbose: TRUE and its Data Type is ". gettype($verbose) . "<br/>";
}else{
    echo "verbose: FALSE and its Data Type is ". gettype($verbose) . "<br/>";
}
if ($test_mode) {
    echo "test_mode: TRUE and its Data Type is ". gettype($test_mode) . "<br/>";
}else{
    echo "test_mode: FALSE and its Data Type is ". gettype($test_mode) . "<br/>";
}

if ($debug_mode) {
    echo "debug_mode: TRUE and its Data Type is ". gettype($debug_mode) . "<br/>";
}else{
    echo "debug_mode: FALSE and its Data Type is ". gettype($debug_mode) . "<br/>";
}
echo "log_file_path: {$log_file_path} and its Data Type is ". gettype($log_file_path) . "<br/>";
if ($send_notifications) {
    echo "send_notifications: TRUE and its Data Type is ". gettype($send_notifications) . "<br/>";
}else{
    echo "send_notifications: FALSE and its Data Type is ". gettype($send_notifications) . "<br/>";
}
/*OR we can manually create and assign variables to the respective key/value pair:
$host = $results["host"];
$server_id = $result["server_id"];
etc...
*/
?>
\$\endgroup\$
6
\$\begingroup\$

I've made a more "functional" version of your code:

but before, I explain why I have made theses changes:

These attributes are useless and can open side effects in your class:

private $fileContents;  //For storing raw file contents
private $resultArray = array(); //For storing raw configuration settings without comments
private $parsedArray = array(); 

Theses two lines can be replaced by a call to the built-in readfile function:

 $this->fileContents = file_get_contents($path); //get config.ini contents
 $this->resultArray = explode("\n", $this->fileContents);

The second foreach can be avoided if you gather all the operation in the first foreach.

And in order to make the differents operations more explicit, I've moved them to their own function extractKey and extractValue.

Finally, I've used a reducing method to avoid intermediate variable and made the code more compact. (In association with a filter function in the linesWithConfig to get only the interesting lines).

The idea behind the split in multiple functions, is to be able to easily understand the code and make changes simpler.

<?php

class ParseConfig  {

    public function parseFile($path){
        return array_reduce($this->linesWithConfig(readfile($path)),
            function($carry, $value){
                $carry[$this->extractKey($value)] = $this->applyType($this->extractValue($value));
        }, []);
    }

    private function linesWithConfig($config_array)
    {
        return array_filter($config_array,
            function($value) {
                return (bool)(strpos($value, '='));
        });
    }

    private function extractKey($string)
    {
        return trim(strstr($string, '=', true));
    }

    private function extractValue($string)
    {
        return trim(substr(strstr($string, '='), 1));
    }

    private function applyType($value){
        if (is_numeric($value) && strpos($value, '.')) {
            return (double)$value;
        } elseif(is_numeric($value)) {
            return (int)$value;
        }elseif(stristr($value,"true") || stristr($value,"yes") || stristr($value,"on")){
            return (bool)true;
        }elseif(stristr($value, "false") || stristr($value, "no") || stristr($value,"off")){
            return (bool)false;
        }else{
            return (string)$value;
        }
    }
}

Please, tell me if there is something you don't understand in my solution.

\$\endgroup\$
3
\$\begingroup\$

The ParseConfig class doesn't need any of the member variables: $fileContents, $resultArray, $parsedArray. These variables are only used by the parsing process in the parseFile function. Therefore they should be local variables in that function. This will also simplify the writing style, you don't need all the $this-> when you convert the variables to locals.

In determineType, you don't need to cast true and false to bool. These are boolean literals, their type is already boolean without the cast.

The way you print variables in the driver is very repetitive. Instead of this:

if ($verbose) {
    echo "verbose: TRUE and its Data Type is ". gettype($verbose) . "<br/>";
}else{
    echo "verbose: FALSE and its Data Type is ". gettype($verbose) . "<br/>";
}

This would be better, with less duplication:

echo "verbose: " . $verbose . " and its Data Type is ". gettype($verbose) . "<br/>";
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.