diff --git a/.gitattributes b/.gitattributes index 516c22d..dd9dfa6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,8 @@ -spec/ export-ignore tests/ export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.scrutinizer.yml export-ignore -.travis.yml export-ignore CONTRIBUTING.md export-ignore -phpspec.yml.dist export-ignore phpunit.xml.dist export-ignore +.github/ export-ignore +UPGRADE-2.0.md export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f7b8104 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: composer + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + versioning-strategy: widen diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..53aad83 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +name: Test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +permissions: + contents: read + +jobs: + test: + strategy: + fail-fast: false + matrix: + php-versions: [ '8.2', '8.3', '8.4', '8.5' ] + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + tools: composer:v2 + extensions: zip, gd, xml, mbstring + + - name: Validate Composer metadata + run: | + if [ -f composer.lock ]; then + composer validate --strict --no-interaction + else + composer validate --strict --no-check-lock --no-interaction + fi + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer packages + uses: actions/cache@v6 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ matrix.php-versions }}-${{ hashFiles('**/composer.json', '**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer-${{ matrix.php-versions }}- + + - name: Install dependencies + run: | + if [ -f composer.lock ]; then + composer install --prefer-dist --no-progress --no-interaction + else + composer update --prefer-dist --no-progress --no-interaction + fi + + - name: Run test suite + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 9780341..d75da34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.phar composer.lock phpspec.yml phpunit.xml +vendor/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index acbda73..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,7 +0,0 @@ -checks: - php: - code_rating: true - duplication: true - -tools: - external_code_coverage: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa84aa6..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: php - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - hhvm - -matrix: - allow_failures: - - php: hhvm - include: - - php: 5.6 - env: - - COMPOSER_FLAGS="--prefer-stable" - - COVERAGE=true - - PHPUNIT_FLAGS="--coverage-clover=coverage.clover" - -install: - - travis_retry composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction - -script: - - vendor/bin/phpunit ${PHPUNIT_FLAGS} - -after_script: - - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi diff --git a/README.md b/README.md index d739ba3..ee6cf55 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,29 @@ # portphp/spreadsheet [![Latest Version](https://img.shields.io/github/release/portphp/spreadsheet.svg?style=flat-square)](https://github.com/portphp/spreadsheet/releases) -[![Build Status](https://travis-ci.org/portphp/spreadsheet.svg)](https://travis-ci.org/portphp/spreadsheet) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/portphp/spreadsheet/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/portphp/spreadsheet/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/portphp/spreadsheet/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/portphp/spreadsheet/?branch=master) +[![CI](https://github.com/portphp/spreadsheet/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/portphp/spreadsheet/actions/workflows/test.yml) +[![PHP Version](https://img.shields.io/packagist/php-v/portphp/spreadsheet.svg?style=flat-square)](https://packagist.org/packages/portphp/spreadsheet) -[PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) reader and writer -for [Port](https://github.com/portphp). +**Requirements:** PHP ^8.2 (tested on 8.2–8.5). Requires `portphp/portphp` ^2.0 and PhpSpreadsheet ^1.29 || ^2–^5. -## Installation +[PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) reader and writer for [Port](https://github.com/portphp). + +> Prefer this package over `portphp/excel` (PHPExcel, abandoned). -Open a command console, enter your project directory and execute the -following command to download the latest stable version of this package: +## Installation ```bash $ composer require portphp/spreadsheet ``` -This command requires you to have Composer installed globally, as explained -in the [installation chapter](https://getcomposer.org/doc/00-intro.md) -of the Composer documentation. - ## Documentation -Documentation is available at http://docs.portphp.org. +Documentation is available at https://portphp.readthedocs.io. ## Issues and feature requests Please report issues and request features at https://github.com/portphp/portphp/issues. -## Contributing - -Contributions are very welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for -details. Thanks to [everyone who has contributed](https://github.com/portphp/spreadsheet/graphs/contributors) -already. - -## Security - -If you discover any security-related issues, please contact us at -[security@portphp.org](mailto:security@portphp.org). - ## License This package is licensed under the [MIT license](LICENSE). diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md new file mode 100644 index 0000000..874bbae --- /dev/null +++ b/UPGRADE-2.0.md @@ -0,0 +1,9 @@ +# Upgrade from 1.x to 2.0 + +- Minimum PHP is **8.2** (`^8.2`). +- Requires `portphp/portphp` **^2.0**. +- Writer methods use `: void` return types from the core interfaces. +- Iterator methods on `SpreadsheetReader` use PHP 8 return types (`mixed`, `void`, etc.). +- `phpoffice/phpspreadsheet` supports **^1.29 || ^2 || ^3 || ^4 || ^5** (uses coordinate API compatible with 2+). +- CI is GitHub Actions (Travis/Scrutinizer removed). +- PHPUnit 9 replaces the old PHPUnit 4 suite; phpspec is no longer a dependency. diff --git a/composer.json b/composer.json index 29eba03..25d1bd4 100644 --- a/composer.json +++ b/composer.json @@ -20,12 +20,12 @@ "support": { "issues": "https://github.com/portphp/portphp/issues", "source": "https://github.com/portphp/spreadsheet", - "docs": "http://docs.portphp.org" + "docs": "https://portphp.readthedocs.io" }, "require": { - "php": ">=5.6.0", - "portphp/portphp": "^1.3", - "phpoffice/phpspreadsheet": "^1.3" + "php": "^8.2", + "portphp/portphp": "^2.0", + "phpoffice/phpspreadsheet": "^1.29 || ^2.0 || ^3.0 || ^4.0 || ^5.0" }, "autoload": { "psr-4": { @@ -33,8 +33,7 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.0", - "phpspec/phpspec": "^2.1" + "phpunit/phpunit": "^9.6" }, "autoload-dev": { "psr-4": { @@ -43,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } } } diff --git a/phpspec.yml.dist b/phpspec.yml.dist deleted file mode 100644 index bf0be48..0000000 --- a/phpspec.yml.dist +++ /dev/null @@ -1,5 +0,0 @@ -suites: - library_suite: - namespace: Port\Spreadsheet - psr4_prefix: Port\Spreadsheet -formatter.name: pretty diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6653884..315d824 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,14 @@ - - + cacheResult="false" + failOnWarning="true" + failOnRisky="true"> - ./tests/ + ./tests diff --git a/src/SpreadsheetReader.php b/src/SpreadsheetReader.php index 1a42b96..1074102 100644 --- a/src/SpreadsheetReader.php +++ b/src/SpreadsheetReader.php @@ -65,17 +65,15 @@ class SpreadsheetReader implements CountableReader, \SeekableIterator */ protected $worksheet; - // phpcs:disable Generic.Files.LineLength.MaxExceeded /** * @param \SplFileObject $file Spreadsheet file - * @param int $headerRowNumber Optional number of header row - * @param int $activeSheet Index of active sheet to read from + * @param int|null $headerRowNumber Optional number of header row + * @param int|null $activeSheet Index of active sheet to read from * @param bool $readOnly If set to false, the reader take care of the spreadsheet formatting (slow) - * @param int $maxRows Maximum number of rows to read + * @param int|null $maxRows Maximum number of rows to read */ - public function __construct(\SplFileObject $file, $headerRowNumber = null, $activeSheet = null, $readOnly = true, $maxRows = null) + public function __construct(\SplFileObject $file, ?int $headerRowNumber = null, ?int $activeSheet = null, bool $readOnly = true, ?int $maxRows = null) { - // phpcs:enable Generic.Files.LineLength.MaxExceeded $reader = IOFactory::createReaderForFile($file->getPathName()); $reader->setReadDataOnly($readOnly); /** @var Spreadsheet $spreadsheet */ @@ -103,7 +101,7 @@ public function __construct(\SplFileObject $file, $headerRowNumber = null, $acti /** * @return int */ - public function count() + public function count(): int { $count = count($this->worksheet); if (null !== $this->headerRowNumber) { @@ -122,7 +120,7 @@ public function count() * * @author Derek Chafin */ - public function current() + public function current(): mixed { $row = $this->worksheet[$this->pointer]; @@ -141,7 +139,7 @@ public function current() * * @return array */ - public function getColumnHeaders() + public function getColumnHeaders(): array { return $this->columnHeaders; } @@ -151,9 +149,9 @@ public function getColumnHeaders() * * @param int $number * - * @return array + * @return array|null */ - public function getRow($number) + public function getRow(int $number): mixed { $this->seek($number); @@ -165,17 +163,15 @@ public function getRow($number) * * @return int */ - public function key() + public function key(): mixed { return $this->pointer; } /** * Move forward to next element - * - * @return void Any returned value is ignored. */ - public function next() + public function next(): void { $this->pointer++; } @@ -186,10 +182,8 @@ public function next() * If a header row has been set, the pointer is set just below the header * row. That way, when you iterate over the rows, that header row is * skipped. - * - * @return void Any returned value is ignored. */ - public function rewind() + public function rewind(): void { if (null === $this->headerRowNumber) { $this->pointer = 0; @@ -204,10 +198,8 @@ public function rewind() * @link http://php.net/manual/en/seekableiterator.seek.php * * @param int $pointer The position to seek to. - * - * @return void Any returned value is ignored. */ - public function seek($pointer) + public function seek(int $pointer): void { $this->pointer = $pointer; } @@ -216,10 +208,8 @@ public function seek($pointer) * Set column headers * * @param array $columnHeaders - * - * @return void Any returned value is ignored. */ - public function setColumnHeaders(array $columnHeaders) + public function setColumnHeaders(array $columnHeaders): void { $this->columnHeaders = $columnHeaders; } @@ -228,10 +218,8 @@ public function setColumnHeaders(array $columnHeaders) * Set header row number * * @param int $rowNumber Number of the row that contains column header names - * - * @return void Any returned value is ignored. */ - public function setHeaderRowNumber($rowNumber) + public function setHeaderRowNumber(int $rowNumber): void { $this->headerRowNumber = $rowNumber; $this->columnHeaders = $this->worksheet[$rowNumber]; @@ -240,10 +228,9 @@ public function setHeaderRowNumber($rowNumber) /** * Checks if current position is valid * - * @return bool The return value will be casted to boolean and then evaluated. - * Returns true on success or false on failure. + * @return bool */ - public function valid() + public function valid(): bool { return isset($this->worksheet[$this->pointer]); } diff --git a/src/SpreadsheetReaderFactory.php b/src/SpreadsheetReaderFactory.php index 086aa97..4e7f8cd 100644 --- a/src/SpreadsheetReaderFactory.php +++ b/src/SpreadsheetReaderFactory.php @@ -24,6 +24,7 @@ */ namespace Port\Spreadsheet; +use Port\Reader; use Port\Reader\ReaderFactory; /** @@ -34,20 +35,20 @@ class SpreadsheetReaderFactory implements ReaderFactory { /** - * @var int + * @var int|null */ protected $activeSheet; /** - * @var int + * @var int|null */ protected $headerRowNumber; /** - * @param int $headerRowNumber - * @param int $activeSheet + * @param int|null $headerRowNumber + * @param int|null $activeSheet */ - public function __construct($headerRowNumber = null, $activeSheet = null) + public function __construct(?int $headerRowNumber = null, ?int $activeSheet = null) { $this->headerRowNumber = $headerRowNumber; $this->activeSheet = $activeSheet; @@ -58,7 +59,7 @@ public function __construct($headerRowNumber = null, $activeSheet = null) * * @return SpreadsheetReader */ - public function getReader(\SplFileObject $file) + public function getReader(\SplFileObject $file): Reader { return new SpreadsheetReader($file, $this->headerRowNumber, $this->activeSheet); } diff --git a/src/SpreadsheetWriter.php b/src/SpreadsheetWriter.php index 05d471e..ab3b35d 100644 --- a/src/SpreadsheetWriter.php +++ b/src/SpreadsheetWriter.php @@ -24,6 +24,7 @@ */ namespace Port\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use Port\Writer; @@ -67,11 +68,11 @@ class SpreadsheetWriter implements Writer /** * @param \SplFileObject $file File - * @param string $sheet Sheet title (optional) + * @param string|null $sheet Sheet title (optional) * @param string $type Spreadsheet file type (defaults to Xlsx) * @param bool $prependHeaderRow */ - public function __construct(\SplFileObject $file, $sheet = null, $type = 'Xlsx', $prependHeaderRow = false) + public function __construct(\SplFileObject $file, ?string $sheet = null, string $type = 'Xlsx', bool $prependHeaderRow = false) { $this->filename = $file->getPathname(); $this->sheet = $sheet; @@ -81,10 +82,8 @@ public function __construct(\SplFileObject $file, $sheet = null, $type = 'Xlsx', /** * Wrap up the writer after all items have been written - * - * @return void Any returned value is ignored. */ - public function finish() + public function finish(): void { $writer = IOFactory::createWriter($this->spreadsheet, $this->type); $writer->save($this->filename); @@ -92,10 +91,8 @@ public function finish() /** * Prepare the writer before writing the items - * - * @return void Any returned value is ignored. */ - public function prepare() + public function prepare(): void { $reader = IOFactory::createReader($this->type); if ($reader->canRead($this->filename)) { @@ -116,10 +113,8 @@ public function prepare() * Write one data item * * @param array $item The data item with converted values - * - * @return void Any returned value is ignored. */ - public function writeItem(array $item) + public function writeItem(array $item): void { $count = count($item); @@ -127,7 +122,8 @@ public function writeItem(array $item) $headers = array_keys($item); for ($i = 0; $i < $count; $i++) { - $this->spreadsheet->getActiveSheet()->setCellValueByColumnAndRow($i + 1, $this->row, $headers[$i]); + $col = Coordinate::stringFromColumnIndex($i + 1); + $this->spreadsheet->getActiveSheet()->setCellValue($col . $this->row, $headers[$i]); } $this->row++; } @@ -135,7 +131,8 @@ public function writeItem(array $item) $values = array_values($item); for ($i = 0; $i < $count; $i++) { - $this->spreadsheet->getActiveSheet()->setCellValueByColumnAndRow($i + 1, $this->row, $values[$i]); + $col = Coordinate::stringFromColumnIndex($i + 1); + $this->spreadsheet->getActiveSheet()->setCellValue($col . $this->row, $values[$i]); } $this->row++; diff --git a/tests/SpreadsheetReaderFactoryTest.php b/tests/SpreadsheetReaderFactoryTest.php index 630c763..f3f059b 100644 --- a/tests/SpreadsheetReaderFactoryTest.php +++ b/tests/SpreadsheetReaderFactoryTest.php @@ -29,12 +29,12 @@ /** * {@inheritDoc} */ -class SpreadsheetReaderFactoryTest extends \PHPUnit_Framework_TestCase +class SpreadsheetReaderFactoryTest extends \PHPUnit\Framework\TestCase { /** * {@inheritDoc} */ - public function setUp() + public function setUp(): void { if (!extension_loaded('zip')) { $this->markTestSkipped(); diff --git a/tests/SpreadsheetReaderTest.php b/tests/SpreadsheetReaderTest.php index 56ddf93..a23eacd 100644 --- a/tests/SpreadsheetReaderTest.php +++ b/tests/SpreadsheetReaderTest.php @@ -29,12 +29,12 @@ /** * {@inheritDoc} */ -class SpreadsheetReaderTest extends \PHPUnit_Framework_TestCase +class SpreadsheetReaderTest extends \PHPUnit\Framework\TestCase { /** * {@inheritDoc} */ - public function setUp() + public function setUp(): void { if (!extension_loaded('zip')) { $this->markTestSkipped(); @@ -247,7 +247,7 @@ public function testIterateWithoutHeaders() public function testMaxRowNumb() { $file = new \SplFileObject(__DIR__.'/fixtures/data_no_column_headers.xls'); - $reader = new SpreadsheetReader($file, null, null, null, 1000); + $reader = new SpreadsheetReader($file, null, null, true, 1000); $this->assertEquals(3, $reader->count()); // Without $maxRows, this faulty file causes OOM because of an extremely @@ -255,7 +255,7 @@ public function testMaxRowNumb() $file = new \SplFileObject(__DIR__.'/fixtures/data_extreme_last_row.xlsx'); $max = 5; - $reader = new SpreadsheetReader($file, null, null, null, $max); + $reader = new SpreadsheetReader($file, null, null, true, $max); $this->assertEquals($max, $reader->count()); } diff --git a/tests/SpreadsheetWriterTest.php b/tests/SpreadsheetWriterTest.php index 84b1dc8..6ed7218 100644 --- a/tests/SpreadsheetWriterTest.php +++ b/tests/SpreadsheetWriterTest.php @@ -30,12 +30,12 @@ /** * {@inheritDoc} */ -class SpreadsheetWriterTest extends \PHPUnit_Framework_TestCase +class SpreadsheetWriterTest extends \PHPUnit\Framework\TestCase { /** * {@inheritDoc} */ - public function setUp() + public function setUp(): void { if (!extension_loaded('zip')) { $this->markTestSkipped(); @@ -50,7 +50,7 @@ public function setUp() */ public function testHeaderNotPrependedByDefault() { - $file = tempnam(sys_get_temp_dir(), null); + $file = tempnam(sys_get_temp_dir(), 'portphp'); $writer = new SpreadsheetWriter(new \SplFileObject($file, 'w'), null, 'Xlsx'); $writer->prepare(); @@ -77,7 +77,7 @@ public function testHeaderNotPrependedByDefault() */ public function testHeaderPrependedWhenOptionSetToTrue() { - $file = tempnam(sys_get_temp_dir(), null); + $file = tempnam(sys_get_temp_dir(), 'portphp'); $writer = new SpreadsheetWriter(new \SplFileObject($file, 'w'), null, 'Xlsx', true); $writer->prepare(); @@ -103,7 +103,7 @@ public function testHeaderPrependedWhenOptionSetToTrue() */ public function testWriteItemAppendWithSheetTitle() { - $file = tempnam(sys_get_temp_dir(), null); + $file = tempnam(sys_get_temp_dir(), 'portphp'); $writer = new SpreadsheetWriter(new \SplFileObject($file, 'w'), 'Sheet 1'); @@ -150,13 +150,14 @@ public function testWriteItemAppendWithSheetTitle() */ public function testWriteItemWithoutSheetTitle() { - $outputFile = new \SplFileObject(tempnam(sys_get_temp_dir(), null)); - $writer = new SpreadsheetWriter($outputFile); + $path = tempnam(sys_get_temp_dir(), 'portphp'); + $writer = new SpreadsheetWriter(new \SplFileObject($path, 'w')); $writer->prepare(); - $writer->writeItem(array('first', 'last')); - $writer->finish(); + + $excel = IOFactory::load($path); + $this->assertEquals(array('first', 'last'), $excel->getActiveSheet()->toArray()[0]); } }