Completed
Pull Request — master (#3212)
by Sergei
49:12 queued 45:02
created

SqliteSchemaManager::dropForeignKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\FetchMode;
24
use Doctrine\DBAL\Types\StringType;
25
use Doctrine\DBAL\Types\TextType;
26
use Doctrine\DBAL\Types\Type;
27
use const CASE_LOWER;
28
use function array_change_key_case;
29
use function array_map;
30
use function array_reverse;
31
use function array_values;
32
use function explode;
33
use function file_exists;
34
use function preg_match;
35
use function preg_match_all;
36
use function preg_quote;
37
use function preg_replace;
38
use function rtrim;
39
use function sprintf;
40
use function str_replace;
41
use function strpos;
42
use function strtolower;
43
use function trim;
44
use function unlink;
45
use function usort;
46
47
/**
48
 * Sqlite SchemaManager.
49
 *
50
 * @author Konsta Vesterinen <[email protected]>
51
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
52
 * @author Jonathan H. Wage <[email protected]>
53
 * @author Martin Hasoň <[email protected]>
54
 * @since  2.0
55
 */
56
class SqliteSchemaManager extends AbstractSchemaManager
57
{
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function dropDatabase($database)
62
    {
63
        if (file_exists($database)) {
64
            unlink($database);
65
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function createDatabase($database)
72
    {
73
        $params = $this->_conn->getParams();
74
        $driver = $params['driver'];
75
        $options = [
76
            'driver' => $driver,
77
            'path' => $database
78
        ];
79
        $conn = \Doctrine\DBAL\DriverManager::getConnection($options);
80
        $conn->connect();
81
        $conn->close();
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function renameTable($name, $newName)
88
    {
89
        $tableDiff = new TableDiff($name);
90
        $tableDiff->fromTable = $this->listTableDetails($name);
91
        $tableDiff->newName = $newName;
92
        $this->alterTable($tableDiff);
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function createForeignKey(ForeignKeyConstraint $foreignKey, $table)
99
    {
100
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
101
        $tableDiff->addedForeignKeys[] = $foreignKey;
102
103
        $this->alterTable($tableDiff);
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table)
110
    {
111
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
112
        $tableDiff->changedForeignKeys[] = $foreignKey;
113
114
        $this->alterTable($tableDiff);
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function dropForeignKey($foreignKey, $table)
121
    {
122
        $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table);
123
        $tableDiff->removedForeignKeys[] = $foreignKey;
124
125
        $this->alterTable($tableDiff);
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function listTableForeignKeys($table, $database = null)
132
    {
133
        if (null === $database) {
134
            $database = $this->_conn->getDatabase();
135
        }
136
        $sql = $this->_platform->getListTableForeignKeysSQL($table, $database);
0 ignored issues
show
Unused Code introduced by
The call to Doctrine\DBAL\Platforms\...stTableForeignKeysSQL() has too many arguments starting with $database. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
        /** @scrutinizer ignore-call */ 
137
        $sql = $this->_platform->getListTableForeignKeysSQL($table, $database);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
137
        $tableForeignKeys = $this->_conn->fetchAll($sql);
138
139
        if ( ! empty($tableForeignKeys)) {
140
            $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
141
            $createSql = $createSql[0]['sql'] ?? '';
142
143
            if (preg_match_all('#
144
                    (?:CONSTRAINT\s+([^\s]+)\s+)?
145
                    (?:FOREIGN\s+KEY[^\)]+\)\s*)?
146
                    REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))?
147
                    (?:
148
                        [^,]*?
149
                        (NOT\s+DEFERRABLE|DEFERRABLE)
150
                        (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
151
                    )?#isx',
152
                    $createSql, $match)) {
153
154
                $names = array_reverse($match[1]);
155
                $deferrable = array_reverse($match[2]);
156
                $deferred = array_reverse($match[3]);
157
            } else {
158
                $names = $deferrable = $deferred = [];
159
            }
160
161
            foreach ($tableForeignKeys as $key => $value) {
162
                $id = $value['id'];
163
                $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id;
164
                $tableForeignKeys[$key]['deferrable']      = isset($deferrable[$id]) && strtolower($deferrable[$id]) === 'deferrable';
165
                $tableForeignKeys[$key]['deferred']        = isset($deferred[$id]) && strtolower($deferred[$id]) === 'deferred';
166
            }
167
        }
168
169
        return $this->_getPortableTableForeignKeysList($tableForeignKeys);
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175 36
    protected function _getPortableTableDefinition($table)
176
    {
177 36
        return $table['name'];
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     *
183
     * @license New BSD License
184
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
185
     */
186 36
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
187
    {
188 36
        $indexBuffer = [];
189
190
        // fetch primary
191 36
        $stmt = $this->_conn->executeQuery("PRAGMA TABLE_INFO ('$tableName')");
192 36
        $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
193
194
        usort($indexArray, function($a, $b) {
195
            if ($a['pk'] == $b['pk']) {
196
                return $a['cid'] - $b['cid'];
197
            }
198
199
            return $a['pk'] - $b['pk'];
200 36
        });
201 36
        foreach ($indexArray as $indexColumnRow) {
202 36
            if ($indexColumnRow['pk'] != "0") {
203 36
                $indexBuffer[] = [
204 36
                    'key_name' => 'primary',
205
                    'primary' => true,
206
                    'non_unique' => false,
207 36
                    'column_name' => $indexColumnRow['name']
208
                ];
209
            }
210
        }
211
212
        // fetch regular indexes
213 36
        foreach ($tableIndexes as $tableIndex) {
214
            // Ignore indexes with reserved names, e.g. autoindexes
215
            if (strpos($tableIndex['name'], 'sqlite_') !== 0) {
216
                $keyName = $tableIndex['name'];
217
                $idx = [];
218
                $idx['key_name'] = $keyName;
219
                $idx['primary'] = false;
220
                $idx['non_unique'] = $tableIndex['unique']?false:true;
221
222
                $stmt = $this->_conn->executeQuery("PRAGMA INDEX_INFO ('{$keyName}')");
223
                $indexArray = $stmt->fetchAll(FetchMode::ASSOCIATIVE);
224
225
                foreach ($indexArray as $indexColumnRow) {
226
                    $idx['column_name'] = $indexColumnRow['name'];
227
                    $indexBuffer[] = $idx;
228
                }
229
            }
230
        }
231
232 36
        return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    protected function _getPortableTableIndexDefinition($tableIndex)
239
    {
240
        return [
241
            'name' => $tableIndex['name'],
242
            'unique' => (bool) $tableIndex['unique']
243
        ];
244
    }
245
246
    /**
247
     * {@inheritdoc}
248
     */
249 36
    protected function _getPortableTableColumnList($table, $database, $tableColumns)
250
    {
251 36
        $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
252
253
        // find column with autoincrement
254 36
        $autoincrementColumn = null;
255 36
        $autoincrementCount = 0;
256
257 36
        foreach ($tableColumns as $tableColumn) {
258 36
            if ('0' != $tableColumn['pk']) {
259 36
                $autoincrementCount++;
260 36
                if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) {
261 36
                    $autoincrementColumn = $tableColumn['name'];
262
                }
263
            }
264
        }
265
266 36
        if (1 == $autoincrementCount && null !== $autoincrementColumn) {
0 ignored issues
show
introduced by
The condition null !== $autoincrementColumn is always false.
Loading history...
267 36
            foreach ($list as $column) {
268 36
                if ($autoincrementColumn == $column->getName()) {
269 36
                    $column->setAutoincrement(true);
270
                }
271
            }
272
        }
273
274
        // inspect column collation and comments
275 36
        $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'");
276 36
        $createSql = $createSql[0]['sql'] ?? '';
277
278 36
        foreach ($list as $columnName => $column) {
279 36
            $type = $column->getType();
280
281 36
            if ($type instanceof StringType || $type instanceof TextType) {
282
                $column->setPlatformOption('collation', $this->parseColumnCollationFromSQL($columnName, $createSql) ?: 'BINARY');
283
            }
284
285 36
            $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
286
287 36
            if ($comment !== null) {
288
                $type = $this->extractDoctrineTypeFromComment($comment, null);
289
290
                if (null !== $type) {
291
                    $column->setType(Type::getType($type));
292
293
                    $comment = $this->removeDoctrineTypeFromComment($comment, $type);
294
                }
295
296 36
                $column->setComment($comment);
297
            }
298
        }
299
300 36
        return $list;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306 36
    protected function _getPortableTableColumnDefinition($tableColumn)
307
    {
308 36
        $parts = explode('(', $tableColumn['type']);
309 36
        $tableColumn['type'] = trim($parts[0]);
310 36
        if (isset($parts[1])) {
311
            $length = trim($parts[1], ')');
312
            $tableColumn['length'] = $length;
313
        }
314
315 36
        $dbType   = strtolower($tableColumn['type']);
316 36
        $length   = $tableColumn['length'] ?? null;
317 36
        $unsigned = false;
318
319 36
        if (strpos($dbType, ' unsigned') !== false) {
320
            $dbType = str_replace(' unsigned', '', $dbType);
321
            $unsigned = true;
322
        }
323
324 36
        $fixed = false;
325 36
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
326 36
        $default = $tableColumn['dflt_value'];
327 36
        if ($default == 'NULL') {
328
            $default = null;
329
        }
330 36
        if ($default !== null) {
331
            // SQLite returns strings wrapped in single quotes, so we need to strip them
332
            $default = preg_replace("/^'(.*)'$/", '\1', $default);
333
        }
334 36
        $notnull = (bool) $tableColumn['notnull'];
335
336 36
        if ( ! isset($tableColumn['name'])) {
337
            $tableColumn['name'] = '';
338
        }
339
340 36
        $precision = null;
341 36
        $scale = null;
342
343 2
        switch ($dbType) {
344 36
            case 'char':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
345
                $fixed = true;
346
                break;
347 36
            case 'float':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
348 36
            case 'double':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
349 36
            case 'real':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
350 36
            case 'decimal':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
351 36
            case 'numeric':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
352
                if (isset($tableColumn['length'])) {
353
                    if (strpos($tableColumn['length'], ',') === false) {
354
                        $tableColumn['length'] .= ",0";
355
                    }
356
                    list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length']));
357
                }
358
                $length = null;
359
                break;
360
        }
361
362
        $options = [
363 36
            'length'   => $length,
364 36
            'unsigned' => (bool) $unsigned,
365 36
            'fixed'    => $fixed,
366 36
            'notnull'  => $notnull,
367 36
            'default'  => $default,
368 36
            'precision' => $precision,
369 36
            'scale'     => $scale,
370
            'autoincrement' => false,
371
        ];
372
373 36
        return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options);
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379
    protected function _getPortableViewDefinition($view)
380
    {
381
        return new View($view['name'], $view['sql']);
382
    }
383
384
    /**
385
     * {@inheritdoc}
386
     */
387
    protected function _getPortableTableForeignKeysList($tableForeignKeys)
388
    {
389
        $list = [];
390
        foreach ($tableForeignKeys as $value) {
391
            $value = array_change_key_case($value, CASE_LOWER);
392
            $name = $value['constraint_name'];
393
            if ( ! isset($list[$name])) {
394
                if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") {
395
                    $value['on_delete'] = null;
396
                }
397
                if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") {
398
                    $value['on_update'] = null;
399
                }
400
401
                $list[$name] = [
402
                    'name' => $name,
403
                    'local' => [],
404
                    'foreign' => [],
405
                    'foreignTable' => $value['table'],
406
                    'onDelete' => $value['on_delete'],
407
                    'onUpdate' => $value['on_update'],
408
                    'deferrable' => $value['deferrable'],
409
                    'deferred'=> $value['deferred'],
410
                ];
411
            }
412
            $list[$name]['local'][] = $value['from'];
413
            $list[$name]['foreign'][] = $value['to'];
414
        }
415
416
        $result = [];
417
        foreach ($list as $constraint) {
418
            $result[] = new ForeignKeyConstraint(
419
                array_values($constraint['local']), $constraint['foreignTable'],
420
                array_values($constraint['foreign']), $constraint['name'],
421
                [
422
                    'onDelete' => $constraint['onDelete'],
423
                    'onUpdate' => $constraint['onUpdate'],
424
                    'deferrable' => $constraint['deferrable'],
425
                    'deferred'=> $constraint['deferred'],
426
                ]
427
            );
428
        }
429
430
        return $result;
431
    }
432
433
    /**
434
     * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey
435
     * @param \Doctrine\DBAL\Schema\Table|string         $table
436
     *
437
     * @return \Doctrine\DBAL\Schema\TableDiff
438
     *
439
     * @throws \Doctrine\DBAL\DBALException
440
     */
441
    private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $foreignKey is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

441
    private function getTableDiffForAlterForeignKey(/** @scrutinizer ignore-unused */ ForeignKeyConstraint $foreignKey, $table)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
442
    {
443
        if ( ! $table instanceof Table) {
444
            $tableDetails = $this->tryMethod('listTableDetails', $table);
445
            if (false === $table) {
0 ignored issues
show
introduced by
The condition false === $table is always false.
Loading history...
446
                throw new DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table));
447
            }
448
449
            $table = $tableDetails;
450
        }
451
452
        $tableDiff = new TableDiff($table->getName());
453
        $tableDiff->fromTable = $table;
0 ignored issues
show
Documentation Bug introduced by
It seems like $table can also be of type false. However, the property $fromTable is declared as type Doctrine\DBAL\Schema\Table. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
454
455
        return $tableDiff;
456
    }
457
458 288
    private function parseColumnCollationFromSQL(string $column, string $sql) : ?string
459
    {
460 288
        $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' . preg_quote($this->_platform->quoteSingleIdentifier($column))
461 288
            . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
462
463 288
        if (preg_match($pattern, $sql, $match) !== 1) {
464 90
            return null;
465
        }
466
467 198
        return $match[1];
468
    }
469
470 432
    private function parseColumnCommentFromSQL(string $column, string $sql) : ?string
471
    {
472 432
        $pattern = '{[\s(,](?:\W' . preg_quote($this->_platform->quoteSingleIdentifier($column)) . '\W|\W' . preg_quote($column)
473 432
            . '\W)(?:\(.*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
474
475 432
        if (preg_match($pattern, $sql, $match) !== 1) {
476 234
            return null;
477
        }
478
479 198
        $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
480
481 198
        return '' === $comment ? null : $comment;
482
    }
483
}
484