├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── example ├── example.dat └── parsing.php └── library └── Fanatique └── Parser ├── FixedLengthFileParser.php ├── ParserException.php └── ParserInterface.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_STORE 2 | INSTALL.txt 3 | LICENSE.txt 4 | README.txt 5 | demos/ 6 | extras/documentation 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License (http://opensource.org/licenses/MIT) 2 | 3 | Copyright (c) 2014 Alexander Thomas (me@alexander-thomas.net) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 11 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 12 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 13 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 14 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 15 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-fixed-length-file-parser 2 | ============================ 3 | 4 | A parser class for handling fixed length text files in PHP. 5 | 6 | Fixed Length Files (aka poor man's CSV) are plain text files with one data set per row 7 | _but without any delimiter_. 8 | 9 | 01Amy BLUES 10 | 02Bob REDS 11 | ... 12 | 13 | ## Features ## 14 | 15 | This class provides a rather comfortable way to handle this type of file on PHP. 16 | 17 | You can: 18 | 19 | - register a pre flight check to determine whether or not a row has to be parsed 20 | - register a callback to handle each line 21 | - register a chopping map which transforms each row into an assiciative array 22 | 23 | 24 | ## Usage ## 25 | 26 | The following example shows how to transform a fixed length file into an associative array. 27 | The working example can be found in `example/parsing.php`. 28 | 29 | $parser = new \Fanatique\Parser\FixedLengthFileParser(); 30 | 31 | //Set the chopping map (aka where to extract the fields) 32 | $parser->setChoppingMap(array( 33 | array('field_name' => 'id', 'start' => 0, 'length' => 2), 34 | array('field_name' => 'name', 'start' => 2, 'length' => 5), 35 | array('field_name' => 'team', 'start' => 7, 'length' => 5), 36 | )); 37 | 38 | ``field_name`` and ``length`` are required and ``start`` is an optional parameter. If ``start`` is omitted, it will be set to the ``start`` plus ``length`` value of the previous map entry. 39 | 40 | //Set the absolute path to the file 41 | $parser->setFilePath(__DIR__ . '/example.dat'); 42 | 43 | //Parse the file 44 | try { 45 | $parser->parse(); 46 | } catch (\Fanatique\Parser\ParserException $e) { 47 | echo 'ERROR - ' . $e->getMessage() . PHP_EOL; 48 | exit(1); 49 | } 50 | 51 | //Get the content 52 | var_dump($parser->getContent()); 53 | 54 | ### Registering a pre flight check ### 55 | 56 | A pre flight check can be registered to be applied to each row *before* it is parsed. 57 | The closure needs to return a boolean value with: 58 | 59 | - `false`: line needs not to be parsed 60 | - `true`: parse line 61 | 62 | This example ignores any line which md5 sum is `f23f81318ef24f1ba4df4781d79b7849`: 63 | 64 | $linesToIgnore = array('f23f81318ef24f1ba4df4781d79b7849'); 65 | $parser->setPreflightCheck(function($currentLineStr) use($linesToIgnore) { 66 | if (in_array(md5($currentLineStr), $linesToIgnore)) { 67 | //Ignore line 68 | $ret = false; 69 | } else { 70 | //Parse line 71 | $ret = true; 72 | } 73 | return $ret; 74 | } 75 | ); 76 | 77 | ### Registering a callback ### 78 | 79 | Finally you can register a callback which is applied to each parsed line and allows you to process it. 80 | The closure gets the parsed line as an array and it is expected to return an array of the same format. 81 | 82 | $parser->setCallback(function(array $currentLine) { 83 | $currentLine['team'] = ucwords(strtolower($currentLine['team'])); 84 | return $currentLine; 85 | } 86 | ); 87 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fanatique/php-fixed-length-file-parser", 3 | "description": "A parser class for handling fixed length text files in PHP", 4 | "keywords": ["php", "text", "parser", "fixed length"], 5 | "homepage": "https://github.com/fanatique/php-fixed-length-file-parser", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Alexander Thomas", 10 | "email": "me@alexander-thomas.net", 11 | "homepage": "http://alexander-thomas.net" 12 | } 13 | ], 14 | "autoload": { 15 | "psr-0": { 16 | "Fanatique": "library/" 17 | } 18 | }, 19 | "version": "dev-master", 20 | "require": { 21 | "php": ">=5.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/example.dat: -------------------------------------------------------------------------------- 1 | 01Amy BLUES 2 | 02Bob REDS 3 | 03ChuckBLUES 4 | 04Dick BLUES 5 | 05EthelREDS 6 | 06Fred BLUES 7 | 07GillyBLUES 8 | 08Hank REDS 9 | -------------------------------------------------------------------------------- /example/parsing.php: -------------------------------------------------------------------------------- 1 | setChoppingMap(array( 22 | array('field_name' => 'id', 'length' => 2), 23 | array('field_name' => 'name', 'start'=>2, 'length' => 5), 24 | array('field_name' => 'team', 'length' => 5), // start is the sum of name:start(2) plus name:length(5) = 7 25 | )); 26 | 27 | //Set the absolute path to the file 28 | $parser->setFilePath(__DIR__ . '/example.dat'); 29 | 30 | //## 1a. optional features 31 | //Register a closure that determines if a line needs to be parsed 32 | //This example ignores any line which md5 sum is f23f81318ef24f1ba4df4781d79b7849 (which kicks out Gilly) 33 | $linesToIgnore = array('f23f81318ef24f1ba4df4781d79b7849'); 34 | $parser->setPreflightCheck(function($currentLineStr) use($linesToIgnore) { 35 | if (in_array(md5($currentLineStr), $linesToIgnore)) { 36 | //Ignore line 37 | $ret = false; 38 | } else { 39 | //Parse line 40 | $ret = true; 41 | } 42 | return $ret; 43 | } 44 | ); 45 | 46 | 47 | //Register a callback which is applied to each parsed line 48 | $parser->setCallback(function(array $currentLine) { 49 | $currentLine['team'] = ucwords(strtolower($currentLine['team'])); 50 | return $currentLine; 51 | } 52 | ); 53 | 54 | //## 2. Parse 55 | try { 56 | $parser->parse(); 57 | } catch (\Fanatique\Parser\ParserException $e) { 58 | echo 'ERROR - ' . $e->getMessage() . PHP_EOL; 59 | exit(1); 60 | } 61 | 62 | //## 3. Get the content 63 | 64 | var_dump($parser->getContent()); 65 | -------------------------------------------------------------------------------- /library/Fanatique/Parser/FixedLengthFileParser.php: -------------------------------------------------------------------------------- 1 | array( 62 | * array('field_name' => 'id,' 'start' => 0, 'length' => 2), 63 | * ... 64 | * ) 65 | * 66 | * @param array $map 67 | */ 68 | public function setChoppingMap(array $map) 69 | { 70 | $this->choppingMap = $map; 71 | } 72 | 73 | /** 74 | * Setter for the file to be parsed 75 | * @param string $pathToFile /path/to/file.dat 76 | */ 77 | public function setFilePath($pathToFile) 78 | { 79 | $this->file = (string) $pathToFile; 80 | } 81 | 82 | /** 83 | * Setter for registering a closure that 84 | * evaluates if a fetched line needs to be parsed. 85 | * 86 | * The closure needs to 87 | * 91 | * 92 | * @param \Closure $preflightCheck 93 | */ 94 | public function setPreflightCheck(\Closure $preflightCheck) 95 | { 96 | $this->preflightCheck = $preflightCheck; 97 | } 98 | 99 | /** 100 | * Setter method for registering a callback which handles 101 | * each line *after* parsing. 102 | * 103 | * The closure needs to 104 | * 108 | * 109 | * @param \Closure $callback 110 | */ 111 | public function setCallback(\Closure $callback) 112 | { 113 | $this->callback = $callback; 114 | } 115 | 116 | /** 117 | * Returns all lines of the parsed content. 118 | * 119 | * @return array 120 | */ 121 | public function getContent() 122 | { 123 | return $this->content; 124 | } 125 | 126 | /** 127 | * Main method for parsing. 128 | * 129 | * @return void 130 | * @throws ParserException 131 | */ 132 | public function parse() 133 | { 134 | //Check for file parameter 135 | if (!isset($this->file)) { 136 | throw new ParserException('No file was specified!'); 137 | } 138 | 139 | //Check for chopping map 140 | if (!isset($this->choppingMap)) { 141 | throw new ParserException('A Chopping Map MUST be specified!'); 142 | } 143 | 144 | //Save pre check as local variable (as PHP does not recognize closures as class members) 145 | $preflightCheck = $this->preflightCheck; 146 | 147 | //Parse file line by line 148 | $this->content = array(); 149 | $filePointer = fopen($this->file, "r"); 150 | while (!feof($filePointer)) { 151 | $buffer = fgets($filePointer, 4096); 152 | 153 | if (!empty($buffer)) { 154 | // If a pre check was registered and it returns not true - the current line 155 | // does not need to be parsed 156 | if ($preflightCheck instanceof \Closure && $preflightCheck($buffer) !== true) { 157 | continue; 158 | } 159 | 160 | //Pass the current string buffer 161 | $this->content[] = $this->parseLine($buffer); 162 | } 163 | } 164 | fclose($filePointer); 165 | } 166 | 167 | /** 168 | * Handles a single line 169 | * 170 | * @param string $buffer 171 | * @return array 172 | */ 173 | private function parseLine($buffer) 174 | { 175 | $currentLine = array(); 176 | $lastPosition = 0; 177 | $mapEntryCount = count($this->choppingMap); 178 | 179 | //Extract each field from the current line 180 | for ($i = 0; $i < $mapEntryCount; $i++) { 181 | 182 | // if start option was set, use it. otherwise use last known position 183 | $start = isset($this->choppingMap[$i]['start']) ? $this->choppingMap[$i]['start'] : $lastPosition; 184 | 185 | // last entry of map, reset position 186 | $lastPosition = $i === $mapEntryCount-1 ? 0 : $lastPosition = $start + $this->choppingMap[$i]['length']; 187 | 188 | $name = $this->choppingMap[$i]['field_name']; 189 | $currentLine[$name] = substr($buffer, 190 | $start, 191 | $this->choppingMap[$i]['length']); 192 | $currentLine[$name] = trim($currentLine[$name]); 193 | 194 | } 195 | 196 | //Store callback as local variable (as PHP does not recognize closures as class members) 197 | $callback = $this->callback; 198 | 199 | /** 200 | * If a call back function was registered - apply it to the current line 201 | */ 202 | if ($callback instanceof \Closure) { 203 | $currentLine = $callback($currentLine); 204 | } 205 | 206 | return $currentLine; 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /library/Fanatique/Parser/ParserException.php: -------------------------------------------------------------------------------- 1 |