diff --git a/.travis.yml b/.travis.yml index 1c5926702..3ade7065a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ matrix: - DB=PGSQL - PHPCS_TEST=1 - PHPUNIT_TEST=framework + - COMPOSER_INSTALL_ARG="--prefer-lowest" - php: 7.2 env: @@ -49,6 +50,16 @@ matrix: packages: - libonig-dev + - php: nightly + env: + - DB=MYSQL + - PHPUNIT_TEST=framework + - COMPOSER_INSTALL_ARG="--ignore-platform-reqs" + addons: + apt: + packages: + - libonig-dev + - php: 7.3 if: type IN (cron) env: @@ -78,7 +89,7 @@ before_script: - composer require silverstripe/recipe-testing:^1 silverstripe/recipe-core:4.x-dev silverstripe/admin:1.x-dev silverstripe/versioned:1.x-dev --no-update - if [[ $PHPUNIT_TEST == cms ]]; then composer require silverstripe/recipe-cms:4.x-dev --no-update; fi - if [[ $PHPCS_TEST ]]; then composer global require squizlabs/php_codesniffer:^3 --prefer-dist --no-interaction --no-progress --no-suggest -o; fi - - composer install --prefer-source --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile + - composer update --prefer-source --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile $COMPOSER_INSTALL_ARG # Log constants to CI for debugging purposes - php ./tests/dump_constants.php diff --git a/composer.json b/composer.json index 473d82a15..f8b2ef8c9 100644 --- a/composer.json +++ b/composer.json @@ -25,11 +25,11 @@ "bramus/monolog-colored-line-formatter": "~2.0", "composer/installers": "~1.0", "embed/embed": "^3.0", - "league/csv": "^8", + "league/csv": "^8 || ^9", "league/flysystem": "~1.0.12", "m1/env": "^2.1", - "monolog/monolog": "~1.11", - "nikic/php-parser": "^2 || ^3 || ^4", + "monolog/monolog": "~1.16", + "nikic/php-parser": "^3 || ^4", "psr/container": "1.0.0", "psr/container-implementation": "1.0.0", "silverstripe/config": "^1@dev", @@ -54,8 +54,8 @@ "ext-xml": "*" }, "require-dev": { - "phpunit/phpunit": "^5.7", - "sminnee/phpunit-mock-objects": "^3.4.5", + "sminnee/phpunit": "^5.7.29", + "sminnee/phpunit-mock-objects": "^3.4.9", "silverstripe/versioned": "^1", "squizlabs/php_codesniffer": "^3.5" }, diff --git a/src/Control/Middleware/ConfirmationMiddleware/HttpMethodBypass.php b/src/Control/Middleware/ConfirmationMiddleware/HttpMethodBypass.php index 277f3c13b..bc41ea1e4 100644 --- a/src/Control/Middleware/ConfirmationMiddleware/HttpMethodBypass.php +++ b/src/Control/Middleware/ConfirmationMiddleware/HttpMethodBypass.php @@ -48,7 +48,7 @@ class HttpMethodBypass implements Bypass // uppercase and exclude empties $methods = array_reduce( $methods, - static function &(&$result, $method) { + function ($result, $method) { $method = strtoupper(trim($method)); if (strlen($method)) { $result[] = $method; diff --git a/src/Core/ClassInfo.php b/src/Core/ClassInfo.php index e67df316b..dc64b9324 100644 --- a/src/Core/ClassInfo.php +++ b/src/Core/ClassInfo.php @@ -439,7 +439,13 @@ class ClassInfo $tokenName = is_array($token) ? $token[0] : $token; // Get the class name - if ($class === null && is_array($token) && $token[0] === T_STRING) { + if (\defined('T_NAME_QUALIFIED') && is_array($token) && + ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) + ) { + // PHP 8 exposes the FQCN as a single T_NAME_QUALIFIED or T_NAME_FULLY_QUALIFIED token + $class .= $token[1]; + $hadNamespace = true; + } elseif ($class === null && is_array($token) && $token[0] === T_STRING) { $class = $token[1]; } elseif (is_array($token) && $token[0] === T_NS_SEPARATOR) { $class .= $token[1]; diff --git a/src/Core/Config/Middleware/InheritanceMiddleware.php b/src/Core/Config/Middleware/InheritanceMiddleware.php index ea394795c..5b0bf1fd6 100644 --- a/src/Core/Config/Middleware/InheritanceMiddleware.php +++ b/src/Core/Config/Middleware/InheritanceMiddleware.php @@ -32,8 +32,8 @@ class InheritanceMiddleware implements Middleware return $config; } - // Skip if no parent class - $parent = get_parent_class($class); + // Skip if not a class or not parent class + $parent = class_exists($class) ? get_parent_class($class) : null; if (!$parent) { return $config; } diff --git a/src/Core/Convert.php b/src/Core/Convert.php index 206de49ef..5cd41dba7 100644 --- a/src/Core/Convert.php +++ b/src/Core/Convert.php @@ -296,12 +296,17 @@ class Convert * @param string $val * @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered. * false by default. - * @param boolean $disableExternals Disables the loading of external entities. false by default. + * @param boolean $disableExternals Disables the loading of external entities. false by default. No-op in PHP 8. * @return array * @throws Exception */ public static function xml2array($val, $disableDoctypes = false, $disableExternals = false) { + // PHP 8 deprecates libxml_disable_entity_loader() as it is no longer needed + if (\PHP_VERSION_ID >= 80000) { + $disableExternals = false; + } + // Check doctype if ($disableDoctypes && preg_match('/\<\!DOCTYPE.+]\>/', $val)) { throw new InvalidArgumentException('XML Doctype parsing disabled'); diff --git a/src/Core/Injector/InjectionCreator.php b/src/Core/Injector/InjectionCreator.php index 3dc7448ab..44fe99a21 100644 --- a/src/Core/Injector/InjectionCreator.php +++ b/src/Core/Injector/InjectionCreator.php @@ -20,6 +20,8 @@ class InjectionCreator implements Factory } if (count($params)) { + // Remove named keys to ensure that PHP7 and PHP8 interpret these the same way + $params = array_values($params); return $reflector->newInstanceArgs($params); } diff --git a/src/Dev/CsvBulkLoader.php b/src/Dev/CsvBulkLoader.php index 5a3a0a56c..f90f76eca 100644 --- a/src/Dev/CsvBulkLoader.php +++ b/src/Dev/CsvBulkLoader.php @@ -2,6 +2,7 @@ namespace SilverStripe\Dev; +use League\Csv\MapIterator; use League\Csv\Reader; use SilverStripe\Control\Director; use SilverStripe\ORM\DataObject; @@ -76,9 +77,16 @@ class CsvBulkLoader extends BulkLoader $filepath = Director::getAbsFile($filepath); $csvReader = Reader::createFromPath($filepath, 'r'); $csvReader->setDelimiter($this->delimiter); - $csvReader->stripBom(true); - $tabExtractor = function ($row, $rowOffset, $iterator) { + // league/csv 9 + if (method_exists($csvReader, 'skipInputBOM')) { + $csvReader->skipInputBOM(); + // league/csv 8 + } else { + $csvReader->stripBom(true); + } + + $tabExtractor = function ($row, $rowOffset) { foreach ($row as &$item) { // [SS-2017-007] Ensure all cells with leading tab and then [@=+] have the tab removed on import if (preg_match("/^\t[\-@=\+]+.*/", $item)) { @@ -90,8 +98,9 @@ class CsvBulkLoader extends BulkLoader if ($this->columnMap) { $headerMap = $this->getNormalisedColumnMap(); - $remapper = function ($row, $rowOffset, $iterator) use ($headerMap, $tabExtractor) { - $row = $tabExtractor($row, $rowOffset, $iterator); + + $remapper = function ($row, $rowOffset) use ($headerMap, $tabExtractor) { + $row = $tabExtractor($row, $rowOffset); foreach ($headerMap as $column => $renamedColumn) { if ($column == $renamedColumn) { continue; @@ -110,9 +119,18 @@ class CsvBulkLoader extends BulkLoader } if ($this->hasHeaderRow) { - $rows = $csvReader->fetchAssoc(0, $remapper); + if (method_exists($csvReader, 'fetchAssoc')) { + $rows = $csvReader->fetchAssoc(0, $remapper); + } else { + $csvReader->setHeaderOffset(0); + $rows = new MapIterator($csvReader->getRecords(), $remapper); + } } elseif ($this->columnMap) { - $rows = $csvReader->fetchAssoc($headerMap, $remapper); + if (method_exists($csvReader, 'fetchAssoc')) { + $rows = $csvReader->fetchAssoc($headerMap, $remapper); + } else { + $rows = new MapIterator($csvReader->getRecords($headerMap), $remapper); + } } foreach ($rows as $row) { diff --git a/src/Forms/GridField/GridFieldExportButton.php b/src/Forms/GridField/GridFieldExportButton.php index 02b6a6c87..8d5b3b427 100644 --- a/src/Forms/GridField/GridFieldExportButton.php +++ b/src/Forms/GridField/GridFieldExportButton.php @@ -257,6 +257,10 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP } } + if (method_exists($csvWriter, 'getContent')) { + return $csvWriter->getContent(); + } + return (string)$csvWriter; } diff --git a/src/ORM/DataQueryManipulator.php b/src/ORM/DataQueryManipulator.php index dd06cb780..12cf66c5b 100644 --- a/src/ORM/DataQueryManipulator.php +++ b/src/ORM/DataQueryManipulator.php @@ -16,7 +16,7 @@ interface DataQueryManipulator * @param array $queriedColumns * @param SQLSelect $sqlSelect */ - public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlSelect); + public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns, SQLSelect $sqlSelect); /** * Invoked after getFinalisedQuery() @@ -25,5 +25,5 @@ interface DataQueryManipulator * @param array $queriedColumns * @param SQLSelect $sqlQuery */ - public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlQuery); + public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns, SQLSelect $sqlQuery); } diff --git a/src/ORM/ManyManyThroughQueryManipulator.php b/src/ORM/ManyManyThroughQueryManipulator.php index 6fffbbe60..93ff393f4 100644 --- a/src/ORM/ManyManyThroughQueryManipulator.php +++ b/src/ORM/ManyManyThroughQueryManipulator.php @@ -231,7 +231,7 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator * @param array $queriedColumns * @param SQLSelect $sqlSelect */ - public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlSelect) + public function beforeGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns, SQLSelect $sqlSelect) { // Get metadata and SQL from join table $hasManyRelation = $this->getParentRelationship($dataQuery); @@ -281,7 +281,7 @@ class ManyManyThroughQueryManipulator implements DataQueryManipulator * @param array $queriedColumns * @param SQLSelect $sqlQuery */ - public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns = [], SQLSelect $sqlQuery) + public function afterGetFinalisedQuery(DataQuery $dataQuery, $queriedColumns, SQLSelect $sqlQuery) { // Inject final replacement after manipulation has been performed on the base dataquery $joinTableSQL = $dataQuery->getQueryParam('Foreign.JoinTableSQL'); diff --git a/src/Security/PasswordEncryptor.php b/src/Security/PasswordEncryptor.php index 6540a7e21..89f07ebbb 100644 --- a/src/Security/PasswordEncryptor.php +++ b/src/Security/PasswordEncryptor.php @@ -56,7 +56,8 @@ abstract class PasswordEncryptor return new $class; } - $arguments = $encryptors[$algorithm]; + // Don't treat array keys as argument names - keeps PHP 7 and PHP 8 operating similarly + $arguments = array_values($encryptors[$algorithm]); return($refClass->newInstanceArgs($arguments)); } diff --git a/src/View/Embed/EmbedResource.php b/src/View/Embed/EmbedResource.php index 3f867465f..8a1c2127c 100644 --- a/src/View/Embed/EmbedResource.php +++ b/src/View/Embed/EmbedResource.php @@ -29,7 +29,7 @@ class EmbedResource implements Embeddable /** * @var array */ - protected $options; + protected $options = []; /** * @var DispatcherInterface diff --git a/src/i18n/TextCollection/i18nTextCollector.php b/src/i18n/TextCollection/i18nTextCollector.php index 843412dd5..5d3445f7a 100644 --- a/src/i18n/TextCollection/i18nTextCollector.php +++ b/src/i18n/TextCollection/i18nTextCollector.php @@ -570,6 +570,13 @@ class i18nTextCollector if (is_array($token)) { list($id, $text) = $token; + // PHP 8 namespace tokens + if (\defined('T_NAME_QUALIFIED') && in_array($id, [T_NAME_FULLY_QUALIFIED, T_NAME_QUALIFIED])) { + $inNamespace = true; + $currentClass[] = $text; + continue; + } + // Check class if ($id === T_NAMESPACE) { $inNamespace = true; diff --git a/tests/php/Core/ClassInfoTest.php b/tests/php/Core/ClassInfoTest.php index 939aa9cbe..e730ff0a0 100644 --- a/tests/php/Core/ClassInfoTest.php +++ b/tests/php/Core/ClassInfoTest.php @@ -105,7 +105,7 @@ class ClassInfoTest extends SapphireTest public function testNonClassName() { $this->expectException(ReflectionException::class); - $this->expectExceptionMessage('Class IAmAClassThatDoesNotExist does not exist'); + $this->expectExceptionMessageRegExp('/Class "?IAmAClassThatDoesNotExist"? does not exist/'); $this->assertEquals('IAmAClassThatDoesNotExist', ClassInfo::class_name('IAmAClassThatDoesNotExist')); } diff --git a/tests/php/Dev/SapphireTestTest/DataProvider.php b/tests/php/Dev/SapphireTestTest/DataProvider.php index c54ea33ea..922a46c48 100644 --- a/tests/php/Dev/SapphireTestTest/DataProvider.php +++ b/tests/php/Dev/SapphireTestTest/DataProvider.php @@ -42,34 +42,34 @@ class DataProvider implements TestOnly public static function provideEqualLists() { return [ - [ - 'oneParameterOneItem' => [ + 'oneParameterOneItem' => [ + [ ['FirstName' => 'Ingo'], ], self::$oneItemList, ], - [ - 'twoParametersOneItem' => [ + 'twoParametersOneItem' => [ + [ ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], ], self::$oneItemList, ], - [ - 'oneParameterTwoItems' => [ + 'oneParameterTwoItems' => [ + [ ['FirstName' => 'Ingo'], ['FirstName' => 'Sam'], ], self::$twoItemList, ], - [ - 'twoParametersTwoItems' => [ + 'twoParametersTwoItems' => [ + [ ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], ['FirstName' => 'Sam', 'Surname' => 'Minnee'], ], self::$twoItemList, ], - [ - 'mixedParametersTwoItems' => [ + 'mixedParametersTwoItems' => [ + [ ['FirstName' => 'Ingo', 'Surname' => 'Schommer'], ['FirstName' => 'Sam'], ], @@ -85,34 +85,34 @@ class DataProvider implements TestOnly { return [ - [ - 'checkAgainstEmptyList' => [ + 'checkAgainstEmptyList' => [ + [ ['FirstName' => 'Ingo'], ], [], ], - [ - 'oneItemExpectedListContainsMore' => [ + 'oneItemExpectedListContainsMore' => [ + [ ['FirstName' => 'Ingo'], ], self::$twoItemList, ], - [ - 'oneExpectationHasWrontParamter' => [ + 'oneExpectationHasWrontParamter' => [ + [ ['FirstName' => 'IngoXX'], ['FirstName' => 'Sam'], ], self::$twoItemList, ], - [ - 'differentParametersInDifferentItemsAreWrong' => [ + 'differentParametersInDifferentItemsAreWrong' => [ + [ ['FirstName' => 'IngoXXX', 'Surname' => 'Schommer'], ['FirstName' => 'Sam', 'Surname' => 'MinneeXXX'], ], self::$twoItemList, ], - [ - 'differentParametersNotMatching' => [ + 'differentParametersNotMatching' => [ + [ ['FirstName' => 'Daniel', 'Surname' => 'Foo'], ['FirstName' => 'Dan'], ], diff --git a/tests/php/Forms/GridField/GridFieldExportButtonTest.php b/tests/php/Forms/GridField/GridFieldExportButtonTest.php index 5738beb98..222d5f657 100644 --- a/tests/php/Forms/GridField/GridFieldExportButtonTest.php +++ b/tests/php/Forms/GridField/GridFieldExportButtonTest.php @@ -55,7 +55,7 @@ class GridFieldExportButtonTest extends SapphireTest $config = GridFieldConfig::create()->addComponent(new GridFieldExportButton()); $gridField = new GridField('testfield', 'testfield', $list, $config); - $csvReader = Reader::createFromString($button->generateExportFileData($gridField)); + $csvReader = $this->createReader($button->generateExportFileData($gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -69,7 +69,7 @@ class GridFieldExportButtonTest extends SapphireTest $button = new GridFieldExportButton(); $button->setExportColumns(['Name' => 'My Name']); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -89,7 +89,7 @@ class GridFieldExportButtonTest extends SapphireTest $button = new GridFieldExportButton(); $button->setExportColumns(['Name' => 'My Name']); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -108,7 +108,7 @@ class GridFieldExportButtonTest extends SapphireTest } ]); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -125,7 +125,7 @@ class GridFieldExportButtonTest extends SapphireTest 'City' => 'strtolower', ]); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -143,7 +143,7 @@ class GridFieldExportButtonTest extends SapphireTest ]); $button->setCsvHasHeader(false); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -167,7 +167,7 @@ class GridFieldExportButtonTest extends SapphireTest $exportData = $button->generateExportFileData($this->gridField); - $csvReader = Reader::createFromString($exportData); + $csvReader = $this->createReader($exportData); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -183,7 +183,7 @@ class GridFieldExportButtonTest extends SapphireTest 'RugbyTeamNumber' => 'Rugby Team Number' ]); - $csvReader = Reader::createFromString($button->generateExportFileData($this->gridField)); + $csvReader = $this->createReader($button->generateExportFileData($this->gridField)); $bom = $csvReader->getInputBOM(); $this->assertEquals( @@ -191,4 +191,16 @@ class GridFieldExportButtonTest extends SapphireTest (string) $csvReader ); } + + protected function createReader($string) + { + $reader = Reader::createFromString($string); + + // Explicitly set the output BOM in league/csv 9 + if (method_exists($reader, 'getContent')) { + $reader->setOutputBOM(Reader::BOM_UTF8); + } + + return $reader; + } } diff --git a/tests/php/ORM/DataListTest.php b/tests/php/ORM/DataListTest.php index 35e77c726..9658f52d1 100755 --- a/tests/php/ORM/DataListTest.php +++ b/tests/php/ORM/DataListTest.php @@ -4,6 +4,7 @@ namespace SilverStripe\ORM\Tests; use InvalidArgumentException; use SilverStripe\Core\Convert; +use SilverStripe\Core\Injector\InjectorNotFoundException; use SilverStripe\Dev\SapphireTest; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataQuery; @@ -751,24 +752,23 @@ class DataListTest extends SapphireTest $this->assertEquals('Bob', $list->first()->Name, 'First comment should be from Bob'); } - /** - * @expectedException \SilverStripe\Core\Injector\InjectorNotFoundException - * @expectedExceptionMessage Class DataListFilter.Bogus does not exist - */ public function testSimpleFilterWithNonExistingComparisator() { + $this->expectException(InjectorNotFoundException::class); + $this->expectExceptionMessageRegExp('/Class "?DataListFilter.Bogus"? does not exist/'); + $list = TeamComment::get(); $list->filter('Comment:Bogus', 'team comment'); } /** * Invalid modifiers are treated as failed filter construction - * - * @expectedException \SilverStripe\Core\Injector\InjectorNotFoundException - * @expectedExceptionMessage Class DataListFilter.invalidmodifier does not exist */ public function testInvalidModifier() { + $this->expectException(InjectorNotFoundException::class); + $this->expectExceptionMessageRegExp('/Class "?DataListFilter.invalidmodifier"? does not exist/'); + $list = TeamComment::get(); $list->filter('Comment:invalidmodifier', 'team comment'); } diff --git a/tests/php/ORM/MySQLPDOConnectorTest.php b/tests/php/ORM/MySQLPDOConnectorTest.php index bbf648d55..9ce68399d 100644 --- a/tests/php/ORM/MySQLPDOConnectorTest.php +++ b/tests/php/ORM/MySQLPDOConnectorTest.php @@ -45,7 +45,6 @@ class MySQLPDOConnectorTest extends SapphireTest implements TestOnly } /** - * @depends testConnectionCharsetControl * @dataProvider charsetProvider */ public function testConnectionCollationControl($charset, $defaultCollation, $customCollation) diff --git a/tests/php/ORM/MySQLiConnectorTest.php b/tests/php/ORM/MySQLiConnectorTest.php index fd65e502f..001c18cdb 100644 --- a/tests/php/ORM/MySQLiConnectorTest.php +++ b/tests/php/ORM/MySQLiConnectorTest.php @@ -38,7 +38,6 @@ class MySQLiConnectorTest extends SapphireTest implements TestOnly } /** - * @depends testConnectionCharsetControl * @dataProvider charsetProvider */ public function testConnectionCollationControl($charset, $defaultCollation, $customCollation)