diff --git a/.gitattributes b/.gitattributes index 3461ac8..9eeb392 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,11 +12,10 @@ tests export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.stickler.yml export-ignore -.travis.yml export-ignore -phpcs.xml.dist export-ignore +phpcs.xml export-ignore phpstan.neon export-ignore phpstan-baseline.neon export-ignore phpunit.xml.dist export-ignore psalm.xml export-ignore psalm-baseline.xml export-ignore +.phive export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ea52ef..2e410c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,105 +4,19 @@ on: push: branches: - 4.x + - 5.x pull_request: branches: - '*' +permissions: + contents: read + jobs: testsuite: - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - php-version: ['7.4', '8.2'] - prefer-lowest: [''] - include: - - php-version: '7.4' - prefer-lowest: 'prefer-lowest' - - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - extensions: mbstring, intl - coverage: pcov - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Get date part for cache key - id: key-date - run: echo "::set-output name=date::$(date +'%Y-%m')" - - - name: Cache composer dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} - - - name: Composer Install - run: | - if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then - composer update --prefer-lowest --prefer-stable - else - composer update - fi - - - name: Configure PHPUnit matcher - if: matrix.php-version == '7.4' - uses: mheap/phpunit-matcher-action@main - - - name: Run PHPUnit - run: | - if [[ ${{ matrix.php-version }} == '7.4' ]]; then - export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml - else - vendor/bin/phpunit - fi - - - name: Submit code coverage - if: success() && matrix.php-version == '7.4' - uses: codecov/codecov-action@v3 + uses: cakephp/.github/.github/workflows/testsuite-with-db.yml@5.x + secrets: inherit cs-stan: - name: Coding Standard & Static Analysis - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - extensions: mbstring, intl - tools: cs2pr - coverage: none - - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Get date part for cache key - id: key-date - run: echo "::set-output name=date::$(date +'%Y-%m')" - - - name: Cache composer dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} - - - name: Composer install - run: composer stan-setup - - - name: Run PHP CodeSniffer - run: composer cs-check - - - name: Run phpstan - if: success() || failure() - run: vendor/bin/phpstan analyse --error-format=github + uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x + secrets: inherit diff --git a/.phive/phars.xml b/.phive/phars.xml new file mode 100644 index 0000000..d2fe06f --- /dev/null +++ b/.phive/phars.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.stickler.yml b/.stickler.yml deleted file mode 100644 index b8f57db..0000000 --- a/.stickler.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -linters: - phpcs: - standard: CakePHP4 - extensions: 'php' - fixer: true - -fixers: - enable: true - workflow: commit diff --git a/README.md b/README.md index 18c5d80..caa0f5e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Quickly enable CSV output of your model data. -This branch is for CakePHP **4.x**. For details see [version map](https://github.com/FriendsOfCake/cakephp-csvview/wiki#cakephp-version-map). +This branch is for CakePHP **5.x**. For details see [version map](https://github.com/FriendsOfCake/cakephp-csvview/wiki#cakephp-version-map). ## Background @@ -182,18 +182,22 @@ to set how null values should be displayed in the CSV. #### Automatic view class switching -You can use router's extension parsing feature and the `RequestHandlerComponent` to -automatically have the CsvView class switched in as follows. +You can use the controller's content negotiation feature to automatically have +the CsvView class switched in as follows. -Enable `csv` extension parsing for all routes using `Router::extensions('csv')` -in your app's `routes.php` or using `$routes->addExtensions()` within required -scope. +Enable `csv` extension parsing using `$routes->addExtensions(['csv'])` within required +scope in your app's `routes.php`. ```php // PostsController.php -// In your controller's initialize() method: -$this->loadComponent('RequestHandler'); +// Add the CsvView class for content type negotiation +public function initialize(): void +{ + parent::initialize(); + + $this->addViewClasses(['csv' => 'CsvView.Csv']); +} // Controller action public function index() diff --git a/composer.json b/composer.json index d401ee2..c0bbb6a 100644 --- a/composer.json +++ b/composer.json @@ -18,38 +18,37 @@ "role": "Maintainer" }, { - "name":"ADmad", - "role":"Contributor", + "name": "ADmad", + "role": "Contributor", "homepage": "https://github.com/admad" }, { - "name":"Mark Scherer", - "role":"Contributor", + "name": "Mark Scherer", + "role": "Contributor", "homepage": "https://github.com/dereuromark" }, { - "name":"Joshua Paling", - "role":"Contributor", + "name": "Joshua Paling", + "role": "Contributor", "homepage": "https://github.com/joshuapaling" }, { - "name":"Gaurish Sharma", - "role":"Contributor", + "name": "Gaurish Sharma", + "role": "Contributor", "homepage": "https://github.com/gaurish" }, { - "name":"Gregory Gaskill", - "role":"Contributor", - "homepage":"https://github.com/chronon" + "name": "Gregory Gaskill", + "role": "Contributor", + "homepage": "https://github.com/chronon" } ], - "require":{ - "php": ">=7.4", - "cakephp/cakephp": "^4.2" + "require": { + "cakephp/cakephp": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.0", - "cakephp/cakephp-codesniffer": "^4.0" + "phpunit/phpunit": "^10.1", + "cakephp/cakephp-codesniffer": "^5.0" }, "autoload": { "psr-4": { diff --git a/phpcs.xml b/phpcs.xml index ca08438..e747af8 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,7 +1,7 @@ - + - + src/ tests/ diff --git a/phpstan.neon b/phpstan.neon index 91bea65..89d9c92 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ parameters: level: 8 checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false paths: - src/ bootstrapFiles: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9910dce..b3a3168 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,32 +1,19 @@ - - - + - tests/TestCase/ + tests/TestCase/ - - - - - - - - - - - - + + + + + + + + src/ - - src/Plugin.php - - - - + + diff --git a/src/Plugin.php b/src/CsvViewPlugin.php similarity index 54% rename from src/Plugin.php rename to src/CsvViewPlugin.php index 33a783c..e3755c3 100644 --- a/src/Plugin.php +++ b/src/CsvViewPlugin.php @@ -5,50 +5,36 @@ use Cake\Core\BasePlugin; use Cake\Core\PluginApplicationInterface; -use Cake\Event\EventInterface; -use Cake\Event\EventManager; use Cake\Http\ServerRequest; -class Plugin extends BasePlugin +class CsvViewPlugin extends BasePlugin { /** * Plugin name. * * @var string */ - protected $name = 'CsvView'; + protected ?string $name = 'CsvView'; /** * Load routes or not * * @var bool */ - protected $routesEnabled = false; + protected bool $routesEnabled = false; /** * Console middleware * * @var bool */ - protected $consoleEnabled = false; + protected bool $consoleEnabled = false; /** * @inheritDoc */ public function bootstrap(PluginApplicationInterface $app): void { - /** - * Add CsvView to View class map through RequestHandler, if available, on Controller initialisation - * - * @link https://book.cakephp.org/4/en/controllers/components/request-handling.html#using-custom-viewclasses - */ - EventManager::instance()->on('Controller.initialize', function (EventInterface $event) { - $controller = $event->getSubject(); - if ($controller->components()->has('RequestHandler')) { - $controller->RequestHandler->setConfig('viewClassMap.csv', 'CsvView.Csv'); - } - }); - /** * Add a request detector named "csv" to check whether the request was for a CSV, * either through accept header or file extension diff --git a/src/View/CsvView.php b/src/View/CsvView.php index 5c6ebf9..88b1d3b 100644 --- a/src/View/CsvView.php +++ b/src/View/CsvView.php @@ -7,7 +7,6 @@ use Cake\Datasource\EntityInterface; use Cake\Utility\Hash; use Cake\View\SerializedView; -use Exception; /** * A view class that is used for CSV responses. @@ -67,7 +66,7 @@ class CsvView extends SerializedView * * @var string */ - protected $layoutPath = 'csv'; + protected string $layoutPath = 'csv'; /** * CSV views are always located in the 'csv' sub directory for a @@ -75,21 +74,14 @@ class CsvView extends SerializedView * * @var string */ - protected $subDir = 'csv'; - - /** - * Response type. - * - * @var string - */ - protected $_responseType = 'text/csv'; + protected string $subDir = 'csv'; /** * Whether or not to reset static variables in use * * @var bool */ - protected $_resetStaticVariables = false; + protected bool $_resetStaticVariables = false; /** * Iconv extension. @@ -110,14 +102,14 @@ class CsvView extends SerializedView * * @var array */ - protected $bomMap; + protected array $bomMap; /** * BOM first appearance * * @var bool */ - protected $isFirstBom = true; + protected bool $isFirstBom = true; /** * Default config. @@ -146,7 +138,7 @@ class CsvView extends SerializedView * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'extract' => null, 'footer' => null, 'header' => null, @@ -188,6 +180,16 @@ public function initialize(): void parent::initialize(); } + /** + * Mime-type this view class renders as. + * + * @return string The CSV content type. + */ + public static function contentType(): string + { + return 'text/csv'; + } + /** * Serialize view vars. * @@ -195,7 +197,7 @@ public function initialize(): void * need(s) to be serialized * @return string The serialized data or false. */ - protected function _serialize($serialize): string + protected function _serialize(array|string $serialize): string { $this->_renderRow($this->getConfig('header')); $this->_renderContent(); @@ -211,7 +213,7 @@ protected function _serialize($serialize): string * Renders the body of the data to the csv * * @return void - * @throws \Exception + * @throws \Cake\Core\Exception\CakeException */ protected function _renderContent(): void { @@ -224,7 +226,7 @@ protected function _renderContent(): void foreach ((array)$serialize as $viewVar) { if (is_scalar($this->viewVars[$viewVar])) { - throw new Exception("'" . $viewVar . "' is not an array or iteratable object."); + throw new CakeException("'" . $viewVar . "' is not an array or iteratable object."); } foreach ($this->viewVars[$viewVar] as $_data) { @@ -296,7 +298,7 @@ protected function _renderRow(?array $row = null): string * @param array|null $row Row data * @return string|false String with the row in csv-syntax, false on fputscv failure */ - protected function _generateRow(?array $row = null) + protected function _generateRow(?array $row = null): string|false { static $fp = false; diff --git a/tests/Fixture/ArticlesFixture.php b/tests/Fixture/ArticlesFixture.php new file mode 100644 index 0000000..ba27cd2 --- /dev/null +++ b/tests/Fixture/ArticlesFixture.php @@ -0,0 +1,13 @@ + 1, 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y'], + ['author_id' => 3, 'title' => 'Second Article', 'body' => 'Second Article Body', 'published' => 'Y'], + ['author_id' => 1, 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y'], + ]; +} diff --git a/tests/Fixture/AuthorsFixture.php b/tests/Fixture/AuthorsFixture.php new file mode 100644 index 0000000..24ef38d --- /dev/null +++ b/tests/Fixture/AuthorsFixture.php @@ -0,0 +1,14 @@ + 'mariano'], + ['name' => 'nate'], + ['name' => 'larry'], + ['name' => 'garrett'], + ]; +} diff --git a/tests/TestCase/View/CsvViewTest.php b/tests/TestCase/View/CsvViewTest.php index ebaab48..4dad49b 100644 --- a/tests/TestCase/View/CsvViewTest.php +++ b/tests/TestCase/View/CsvViewTest.php @@ -5,17 +5,17 @@ use Cake\Http\Response; use Cake\Http\ServerRequest as Request; -use Cake\I18n\FrozenTime; -use Cake\ORM\TableRegistry; +use Cake\I18n\DateTime; use Cake\TestSuite\TestCase; use CsvView\View\CsvView; +use Exception; /** * CsvViewTest */ class CsvViewTest extends TestCase { - public $fixtures = ['core.Articles', 'core.Authors']; + protected array $fixtures = ['plugin.CsvView.Articles', 'plugin.CsvView.Authors']; /** * @var \CsvView\View\CsvView @@ -34,7 +34,9 @@ class CsvViewTest extends TestCase public function setUp(): void { - FrozenTime::setToStringFormat('yyyy-MM-dd HH:mm:ss'); + parent::setUp(); + + DateTime::setToStringFormat('yyyy-MM-dd HH:mm:ss'); $this->request = new Request(); $this->response = new Response(); @@ -267,7 +269,7 @@ public function testRenderViaExtract() [ 'User' => [ 'username' => 'jose', - 'created' => new FrozenTime('2010-01-05'), + 'created' => new DateTime('2010-01-05'), ], 'Item' => [ 'name' => 'beach', @@ -343,7 +345,7 @@ public function testRenderViaExtractWithCallable() $data = [ [ 'username' => 'jose', - 'created' => new FrozenTime('2010-01-05'), + 'created' => new DateTime('2010-01-05'), 'item' => [ 'name' => 'beach', ], @@ -431,7 +433,7 @@ public function testRenderWithSpecialCharacters() */ public function testPassingQueryAsData() { - $articles = TableRegistry::getTableLocator()->get('Articles'); + $articles = $this->getTableLocator()->get('Articles'); $query = $articles->find(); $this->view->set(['data' => $query]) @@ -439,7 +441,7 @@ public function testPassingQueryAsData() $output = $this->view->render(); $articles->belongsTo('Authors'); - $query = $articles->find('all', ['contain' => 'Authors']); + $query = $articles->find('all', contain: 'Authors'); $_extract = ['title', 'body', 'author.name']; $this->view->set(['data' => $query]) ->setConfig(['extract' => $_extract, 'serialize' => 'data']); @@ -511,7 +513,7 @@ public function testRenderWithCustomNull() */ public function testInvalidViewVarThrowsException() { - $this->expectException(\Exception::class); + $this->expectException(Exception::class); $this->view->set(['data' => 'invaliddata']); $this->view->setConfig('serialize', 'data'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f2f2e40..168d2bf 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,6 +2,7 @@ declare(strict_types=1); use Cake\Core\Configure; +use Cake\TestSuite\Fixture\SchemaLoader; /** * Test suite bootstrap @@ -37,7 +38,13 @@ } Configure::write('App', [ 'namespace' => 'CsvView\Test\App', + 'encoding' => 'UTF-8', 'paths' => [ 'templates' => [dirname(__FILE__) . DS . 'test_app' . DS . 'templates' . DS], ], ]); + +if (getenv('FIXTURE_SCHEMA_METADATA')) { + $loader = new SchemaLoader(); + $loader->loadInternalFile(getenv('FIXTURE_SCHEMA_METADATA')); +} diff --git a/tests/schema.php b/tests/schema.php new file mode 100644 index 0000000..93062fe --- /dev/null +++ b/tests/schema.php @@ -0,0 +1,53 @@ + [ + 'columns' => [ + 'id' => [ + 'type' => 'integer', + ], + 'author_id' => [ + 'type' => 'integer', + 'null' => true, + ], + 'title' => [ + 'type' => 'string', + 'null' => true, + ], + 'body' => 'text', + 'published' => [ + 'type' => 'string', + 'length' => 1, + 'default' => 'N', + ], + ], + 'constraints' => [ + 'primary' => [ + 'type' => 'primary', + 'columns' => [ + 'id', + ], + ], + ], + ], + 'authors' => [ + 'columns' => [ + 'id' => [ + 'type' => 'integer', + ], + 'name' => [ + 'type' => 'string', + 'default' => null, + ], + ], + 'constraints' => [ + 'primary' => [ + 'type' => 'primary', + 'columns' => [ + 'id', + ], + ], + ], + ], +]; diff --git a/tests/test_app/TestApp/Application.php b/tests/test_app/TestApp/Application.php index fca4cdc..d999115 100644 --- a/tests/test_app/TestApp/Application.php +++ b/tests/test_app/TestApp/Application.php @@ -18,6 +18,7 @@ use Cake\Error\Middleware\ErrorHandlerMiddleware; use Cake\Http\BaseApplication; +use Cake\Http\MiddlewareQueue; use Cake\Routing\Middleware\AssetMiddleware; use Cake\Routing\Middleware\RoutingMiddleware; @@ -49,7 +50,7 @@ public function bootstrap(): void * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup. * @return \Cake\Http\MiddlewareQueue The updated middleware queue. */ - public function middleware(\Cake\Http\MiddlewareQueue $middlewareQueue): \Cake\Http\MiddlewareQueue + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { $middlewareQueue // Catch any exceptions in the lower layers,