CsvParser::parse()   F
last analyzed

Complexity

Conditions 16
Paths 578

Size

Total Lines 124

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 124
c 0
b 0
f 0
rs 1.5888
cc 16
nc 578
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * m'Manager | Invoices Management System
5
 * 
6
 * This content is released under the Proprietary License (Proprietary)
7
 *
8
 * Copyright (c) 2017, Eric Claver AKAFFOU - All Rights Reserved
9
 * Unauthorized copying of this file, via any medium is strictly prohibited
10
 * Proprietary and confidential
11
 * 
12
 * @package m'Manager
13
 * @author  Eric Claver AKAFFOU
14
 * @copyright   Copyright (c) 2017, on'Eric Computing, Inc. (https://www.onericcomputing.com/)
15
 * @license https://www.mmanager.fr  Proprietary License
16
 * @link    https://codecanyon.net/item/mmanager-invoices-management-system/19866435?s_rank=1
17
 * @since   Version 1.0.0
18
 * @filesource
19
 */
20
21
namespace Mmanager\Utils;
22
23
use Mmanager\Contract\ParserInterface;
24
25
class CsvParser implements ParserInterface {
26
27
	private $handle = "";
28
	private $filepath = FALSE;
29
	private $column_headers = FALSE;
30
	private $initial_line = 0;
31
	private $delimiter = ",";
32
	private $detect_line_endings = FALSE;
33
34
   /**
35
    * Function that parses a CSV file and returns results
36
    * as an array.
37
    *
38
    * @access  public
39
    * @param   filepath        string  Location of the CSV file
40
    * @param   column_headers  array   Alternate values that will be used for array keys instead of first line of CSV
41
    * @param   detect_line_endings  boolean  When true sets the php INI settings to allow script to detect line endings. Needed for CSV files created on Macs.
42
    * @param   initial_line  integer  Sets the line of the file from which start parsing data.
43
    * @param   delimiter  string  The values delimiter (e.g. ";" or ",").
44
    * @return  array
45
    */
46
	public function parse($filepath=FALSE, $column_headers=FALSE, $detect_line_endings=FALSE, $initial_line=FALSE, $delimiter=FALSE)
47
	{
48
		// Raise memory limit (for big files)
49
		ini_set('memory_limit', '128M');
50
        
51
		// File path
52
		if(! $filepath)
0 ignored issues
show
Bug Best Practice introduced by
The expression $filepath of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
53
		{
54
			$filepath = $this->_get_filepath();    
55
		}
56
		else
57
		{   
58
			// If filepath provided, set it
59
			$this->_set_filepath($filepath);
60
		}
61
62
		// If file doesn't exists, return false
63
		if(! file_exists($filepath))
64
		{
65
			return FALSE;            
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type documented by Mmanager\Utils\CsvParser::parse of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
66
		}
67
68
		// auto detect row endings
69
		if(! $detect_line_endings)
70
		{
71
			$detect_line_endings = $this->_get_detect_line_endings();    
72
		}
73
		else
74
		{   
75
			// If detect_line_endings provided, set it
76
			$this->_set_detect_line_endings($detect_line_endings);
77
		}
78
79
		// If true, auto detect row endings
80
		if($detect_line_endings) 
81
		{
82
			ini_set("auto_detect_line_endings", TRUE);
83
		}
84
85
		// Parse from this line on
86
		if(! $initial_line)
0 ignored issues
show
Bug Best Practice introduced by
The expression $initial_line of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
87
		{
88
			$initial_line = $this->_get_initial_line();    
0 ignored issues
show
Unused Code introduced by
$initial_line is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
89
		}
90
		else
91
		{
92
			$this->_set_initial_line($initial_line);
93
		}
94
95
		// Delimiter
96
		if(! $delimiter)
0 ignored issues
show
Bug Best Practice introduced by
The expression $delimiter of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
97
		{
98
			$delimiter = $this->_get_delimiter();    
0 ignored issues
show
Unused Code introduced by
$delimiter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
99
		}
100
		else
101
		{   
102
			// If delimiter provided, set it
103
			$this->_set_delimiter($delimiter);
104
		}
105
106
		// Column headers
107
		if(! $column_headers)
108
		{
109
			$column_headers = $this->_get_column_headers();    
110
		}
111
		else
112
		{
113
			// If column headers provided, set them
114
			$this->_set_column_headers($column_headers);
115
		}
116
117
		// Open the CSV for reading
118
		$this->_get_handle();
119
        
120
		$row = 0;
121
122
		while (($data = fgetcsv($this->handle, 0, $this->delimiter)) !== FALSE) 
123
		{     
124
			if ($data[0] != NULL) 
125
			{
126
				if($row < $this->initial_line)
127
				{
128
					$row++;
129
					continue;
130
				}
131
132
				// If first row, parse for column_headers
133
				if($row == $this->initial_line)
134
				{
135
					// If column_headers already provided, use them
136
					if($this->column_headers)
137
					{
138
						foreach ($this->column_headers as $key => $value)
0 ignored issues
show
Bug introduced by
The expression $this->column_headers of type boolean is not traversable.
Loading history...
139
						{
140
							$column_headers[$key] = trim($value);
141
						}
142
					}
143
					else // Parse first row for column_headers to use
144
					{
145
						foreach ($data as $key => $value)
146
						{
147
							  $column_headers[$key] = trim($value);
148
						}                
149
					}          
150
				}
151
				else
152
				{
153
					$new_row = $row - $this->initial_line - 1; // needed so that the returned array starts at 0 instead of 1
154
					foreach($column_headers as $key => $value) // assumes there are as many columns as their are title columns
0 ignored issues
show
Bug introduced by
The expression $column_headers of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
155
					{
156
					$result[$new_row][$value] = utf8_encode(trim($data[$key]));
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
157
					}
158
				}
159
            
160
				unset($data);
161
            
162
				$row++;
163
			}
164
		}
165
 
166
		$this->_close_csv();
167
168
		return $result;
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
169
	}
170
171
172
    
173
	/**
174
	 * Sets the "detect_line_endings" flag
175
	 *
176
	 * @access  private
177
	 * @param   detect_line_endings    bool  The flag bit
178
	 * @return  void
179
	 */
180
	private function _set_detect_line_endings($detect_line_endings)
181
	{
182
		$this->detect_line_endings = $detect_line_endings;
183
	}
184
185
	/**
186
	 * Sets the "detect_line_endings" flag
187
	 *
188
	 * @access  public
189
	 * @param   detect_line_endings    bool  The flag bit
190
	 * @return  void
191
	 */
192
	public function detect_line_endings($detect_line_endings)
193
	{
194
		$this->_set_detect_line_endings($detect_line_endings);
195
		return $this;
196
	}
197
198
	/**
199
	 * Gets the "detect_line_endings" flag
200
	 *
201
	 * @access  private
202
	 * @return  bool
203
	 */
204
	private function _get_detect_line_endings()
205
	{
206
		return $this->detect_line_endings;
207
	}
208
209
	/**
210
	 * Sets the initial line from which start to parse the file
211
	 *
212
	 * @access  private
213
	 * @param   initial_line    int  Start parse from this line
214
	 * @return  void
215
	 */
216
	private function _set_initial_line($initial_line)
217
	{
218
	   return $this->initial_line = $initial_line;
219
	}
220
221
	/**
222
	 * Sets the initial line from which start to parse the file
223
	 *
224
	 * @access  public
225
	 * @param   initial_line    int  Start parse from this line
226
	 * @return  void
227
	 */
228
	public function initial_line($initial_line)
229
	{
230
		$this->_set_initial_line($initial_line);
231
		return $this;
232
	}
233
234
	/**
235
	 * Gets the initial line from which start to parse the file
236
	 *
237
	 * @access  private
238
	 * @return  int
239
	 */
240
	private function _get_initial_line()
241
	{
242
		return $this->initial_line;
243
	}
244
245
	/**
246
	 * Sets the values delimiter
247
	 *
248
	 * @access  private
249
	 * @param   initial_line    string  The values delimiter (eg. "," or ";")
250
	 * @return  void
251
	 */
252
	private function _set_delimiter($delimiter)
253
	{
254
		$this->delimiter = $delimiter;
255
	}
256
257
	/**
258
	 * Sets the values delimiter
259
	 *
260
	 * @access  public
261
	 * @param   initial_line    string  The values delimiter (eg. "," or ";")
262
	 * @return  void
263
	 */
264
	public function delimiter($delimiter)
265
	{
266
		$this->_set_delimiter($delimiter);
267
		return $this;
268
	}
269
270
	/**
271
	 * Gets the values delimiter
272
	 *
273
	 * @access  private
274
	 * @return  string
275
	 */
276
	private function _get_delimiter()
277
	{
278
		return $this->delimiter;
279
	}
280
281
	/**
282
	 * Sets the filepath of a given CSV file
283
	 *
284
	 * @access  private
285
	 * @param   filepath    string  Location of the CSV file
286
	 * @return  void
287
	 */
288
	private function _set_filepath($filepath)
289
	{
290
		$this->filepath = $filepath;
0 ignored issues
show
Documentation Bug introduced by
The property $filepath was declared of type boolean, but $filepath is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
291
	}
292
293
	/**
294
	 * Sets the filepath of a given CSV file
295
	 *
296
	 * @access  public
297
	 * @param   filepath    string  Location of the CSV file
298
	 * @return  void
299
	 */
300
	public function filepath($filepath)
301
	{
302
		$this->_set_filepath($filepath);
303
		return $this;
304
	}
305
306
	/**
307
	 * Gets the filepath of a given CSV file
308
	 *
309
	 * @access  private
310
	 * @return  string
311
	 */
312
	private function _get_filepath()
313
	{
314
		return $this->filepath;
315
	}
316
317
   /**
318
    * Sets the alternate column headers that will be used when creating the array
319
    *
320
    * @access  private
321
    * @param   column_headers  array   Alternate column_headers that will be used instead of first line of CSV
322
    * @return  void
323
    */
324
	private function _set_column_headers($column_headers='')
325
	{
326
		if(is_array($column_headers) && !empty($column_headers))
327
		{
328
			$this->column_headers = $column_headers;
0 ignored issues
show
Documentation Bug introduced by
It seems like $column_headers of type array is incompatible with the declared type boolean of property $column_headers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
329
		}
330
	}
331
332
	/**
333
	 * Sets the alternate column headers that will be used when creating the array
334
	 *
335
	 * @access  public
336
	 * @param   column_headers  array   Alternate column_headers that will be used instead of first line of CSV
337
	 * @return  void
338
	 */
339
	public function column_headers($column_headers)
340
	{
341
		$this->_set_column_headers($column_headers);
342
		return $this;
343
	}
344
345
	/**
346
	 * Gets the alternate column headers that will be used when creating the array
347
	 *
348
	 * @access  private
349
	 * @return  mixed
350
	 */
351
	private function _get_column_headers()
352
	{
353
		return $this->column_headers;
354
	}
355
356
   /**
357
    * Opens the CSV file for parsing
358
    *
359
    * @access  private
360
    * @return  void
361
    */
362
	private function _get_handle()
363
	{
364
		$this->handle = fopen($this->filepath, "r");
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($this->filepath, 'r') of type resource is incompatible with the declared type string of property $handle.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
365
	}
366
367
   /**
368
    * Closes the CSV file when complete
369
    *
370
    * @access  private
371
    * @return  array
372
    */
373
	private function _close_csv()
374
	{
375
		fclose($this->handle);
376
	}    
377
}
378