Failed Conditions
Pull Request — experimental/3.1 (#2199)
by chihiro
82:14 queued 75:34
created

CsvFixture::getFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Eccube\Doctrine\Common\CsvDataFixtures;
4
5
use Doctrine\Common\DataFixtures\FixtureInterface;
6
use Doctrine\Common\Persistence\ObjectManager;
7
use Doctrine\DBAL\Connection;
8
use Doctrine\DBAL\Schema\Table;
9
10
/**
11
 * CSVファイルを扱うためのフィクスチャ.
12
 *
13
 * @see https://github.com/doctrine/data-fixtures/blob/master/lib/Doctrine/Common/DataFixtures/FixtureInterface.php
14
 */
15
class CsvFixture implements FixtureInterface
16
{
17
    /** @var \SplFileObject $file */
18
    protected $file;
19
20
    public function __construct(\SplFileObject $file)
0 ignored issues
show
introduced by
Missing function doc comment
Loading history...
21
    {
22
        $this->file = $file;
23
    }
24
25
    /**
26
     * {@inheritdoc}
27
     */
28
    public function load(ObjectManager $manager)
29
    {
30
        // CSV Reader に設定
31
        $this->file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY);
32
33
        // ヘッダ行を取得
34
        $headers = $this->file->current();
35
        $this->file->next();
36
37
        // ファイル名からテーブル名を取得
38
        $table_name = str_replace('.'.$this->file->getExtension(), '', $this->file->getFilename());
39
        $sql = $this->getSql($table_name, $headers);
0 ignored issues
show
Bug introduced by
It seems like $headers defined by $this->file->current() on line 34 can also be of type string; however, Eccube\Doctrine\Common\C...es\CsvFixture::getSql() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
40
        /** @var Connection $Connection */
41
        $Connection = $manager->getConnection();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Persistence\ObjectManager as the method getConnection() does only exist in the following implementations of said interface: Doctrine\ORM\Decorator\EntityManagerDecorator, Doctrine\ORM\EntityManager.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
42
        $Connection->beginTransaction();
43
44
        // mysqlの場合はNO_AUTO_VALUE_ON_ZEROを設定
45
        if ('mysql' === $Connection->getDatabasePlatform()->getName()) {
46
            $Connection->exec("SET SESSION sql_mode='NO_AUTO_VALUE_ON_ZERO';");
47
        }
48
49
        // TODO エラーハンドリング
50
        $prepare = $Connection->prepare($sql);
51
        while (!$this->file->eof()) {
52
            $rows = $this->file->current();
53
            $index = 1;
54
            // データ行をバインド
55
            foreach ($rows as $col) {
0 ignored issues
show
Bug introduced by
The expression $rows of type string|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...
56
                $col = $col === '' ? null : $col;
57
                $prepare->bindValue($index, $col);
58
                $index++;
59
            }
60
            // batch insert
61
            $result = $prepare->execute();
0 ignored issues
show
Unused Code introduced by
$result 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...
62
            $this->file->next();
63
        }
64
        $Connection->commit();
65
66
        // postgresqlの場合はシーケンスを振り直す
67
        if ('postgresql' === $Connection->getDatabasePlatform()->getName()) {
68
            // テーブル情報を取得
69
            $sm = $Connection->getSchemaManager();
70
            $table = $sm->listTableDetails($table_name);
71
72
            // 主キーがないテーブルはスキップ
73
            if (!$table->hasPrimaryKey()) {
74
                return;
75
            }
76
77
            // 複合主キーのテーブルはスキップ
78
            $pkColumns = $table->getPrimaryKey()->getColumns();
79
            if (count($pkColumns) != 1) {
80
                return;
81
            }
82
83
            // シーケンス名を取得
84
            $pk_name = $pkColumns[0];
85
            $sequence_name = sprintf('%s_%s_seq', $table_name, $pk_name);
86
87
            // シーケンスの存在チェック
88
            $sql = 'SELECT COUNT(*) FROM information_schema.sequences WHERE sequence_name = ?';
89
            $count = $Connection->fetchColumn($sql, [$sequence_name]);
90
            if ($count < 1) {
91
                return;
92
            }
93
94
            // シーケンスを更新
95
            $sql = sprintf('SELECT MAX(%s) FROM %s', $pk_name, $table_name);
96
            $max = $Connection->fetchColumn($sql);
97
            if (is_null($max)) {
98
                // レコードが無い場合は1を初期値に設定
99
                $sql = sprintf("SELECT SETVAL('%s', 1, false)", $sequence_name);
100
            } else {
101
                // レコードがある場合は最大値を設定
102
                $sql = sprintf("SELECT SETVAL('%s', %s)", $sequence_name, $max);
103
            }
104
            $Connection->executeQuery($sql);
105
        }
106
    }
107
108
    /**
109
     * INSERT を生成する.
110
     *
111
     * @param string $table_name テーブル名
112
     * @param array $headers カラム名の配列
0 ignored issues
show
introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
introduced by
Expected 4 spaces after parameter name; 1 found
Loading history...
113
     * @return string INSERT 文
114
     */
115
    public function getSql($table_name, array $headers)
116
    {
117
        return 'INSERT INTO '.$table_name.' ('.implode(', ', $headers).') VALUES ('.implode(', ', array_fill(0, count($headers), '?')).')';
118
    }
119
120
    /**
121
     * 保持している \SplFileObject を返す.
122
     *
123
     * @return \SplFileObject
124
     */
125
    public function getFile()
126
    {
127
        return $this->file;
128
    }
129
}
130