From fa512bc1b943546be53453270c8b8bcd22144358 Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Thu, 20 Mar 2025 12:54:12 +0100 Subject: [PATCH 1/4] support for PHP 8.4 amongst other improvements --- .gitattributes | 16 +++---- .github/workflows/main.yaml | 77 +-------------------------------- .gitignore | 2 + .php_cs => .php-cs-fixer.php | 0 .phpstan.neon | 12 +++++ CHANGELOG.md | 6 +++ LICENSE | 2 +- README.md | 40 +++++++++++------ composer.json | 18 ++++---- phpcs.xml.dist => phpcs.xml | 0 phpunit.xml.dist => phpunit.xml | 0 src/Filesystem.php | 14 +++--- src/Reader.php | 22 ++++++---- src/Writer.php | 9 ++-- tests/ReaderTest.php | 19 ++++---- tests/WriterTest.php | 14 +++--- 16 files changed, 109 insertions(+), 142 deletions(-) rename .php_cs => .php-cs-fixer.php (100%) create mode 100644 .phpstan.neon rename phpcs.xml.dist => phpcs.xml (100%) rename phpunit.xml.dist => phpunit.xml (100%) diff --git a/.gitattributes b/.gitattributes index c540470..b67c7ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,10 @@ * text=auto eol=lf -/tests export-ignore -.editorconfig export-ignore -.gitattributes export-ignore -.gitignore export-ignore -.php_cs export-ignore -.travis.yml export-ignore -phpcs.xml.dist export-ignore -phpunit.xml.dist export-ignore +/tests export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php-cs-fixer.php export-ignore +phpcs.xml.dist export-ignore +phpunit.xml.dist export-ignore +.phpstan.neon export-ignore diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 327717c..e7d07a0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,76 +1 @@ -name: "testing" - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - qa: - name: Quality assurance - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Cache Composer packages - id: composer-cache - uses: actions/cache@v2 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- - - - name: Install dependencies - if: steps.composer-cache.outputs.cache-hit != 'true' - run: composer install --prefer-dist --no-progress - - - name: Coding Standard - run: composer run-script cs - - tests: - name: Tests - runs-on: ubuntu-latest - - strategy: - matrix: - php: - - 7.2 - - 7.3 - - 7.4 - composer-args: [ "" ] - include: - - php: 8.0 - composer-args: --ignore-platform-reqs - fail-fast: false - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - - - name: Cache PHP dependencies - uses: actions/cache@v2 - with: - path: vendor - key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer- - - - name: Install dependencies - run: composer install --prefer-dist --no-progress ${{ matrix.composer-args }} - - - name: Tests - run: composer test - - - name: Tests coverage - run: composer coverage +/src/config/workflow.yaml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 364d1a4..5ae8693 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ vendor composer.lock coverage *.cache +.idea +kit diff --git a/.php_cs b/.php-cs-fixer.php similarity index 100% rename from .php_cs rename to .php-cs-fixer.php diff --git a/.phpstan.neon b/.phpstan.neon new file mode 100644 index 0000000..a3d1fa1 --- /dev/null +++ b/.phpstan.neon @@ -0,0 +1,12 @@ +parameters: + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + level: max + paths: + - src + - tests + ignoreErrors: + - + identifier: 'function.alreadyNarrowedType' + reportUnmatched: false + - "#^Unsafe usage of new static#" diff --git a/CHANGELOG.md b/CHANGELOG.md index ba86514..36600da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [3.0.0] - 2020-12-03 +### Changed +- Updated `league/filesystem` to version 2 for php 7.2, 7.3 and 7.4, and to version 3 for >= php 8.0. +- With these changes, a breaking change is introduced by `league/filesystem`. Now you have to pass a FilesystemOperator interface compliant class to the middlewares. Check README's code examples. + ## [2.0.1] - 2020-12-03 ### Added - Support for PHP 8.0 @@ -69,6 +74,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## 0.1.0 - 2016-10-02 First version +[3.0.0]: https://github.com/middlewares/filesystem/compare/v2.0.1...v3.0.0 [2.0.1]: https://github.com/middlewares/filesystem/compare/v2.0.0...v2.0.1 [2.0.0]: https://github.com/middlewares/filesystem/compare/v1.1.0...v2.0.0 [1.1.0]: https://github.com/middlewares/filesystem/compare/v1.0.0...v1.1.0 diff --git a/LICENSE b/LICENSE index 017c0cd..374fb13 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019 +Copyright (c) 2019-2025 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 3d5f68e..e4eff33 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,30 @@ Example using a ftp storage: ```php use League\Flysystem\Filesystem; -use League\Flysystem\Adapter\Ftp; - -$filesystem = new Filesystem(new Ftp([ - 'host' => 'ftp.example.com', - 'username' => 'username', - 'password' => 'password', - 'port' => 21, - 'root' => '/path/to/root', - 'passive' => true, - 'ssl' => true, - 'timeout' => 30, -])); +use League\Flysystem\Ftp\FtpAdapter; +use League\Flysystem\Ftp\FtpConnectionOptions; + +$adapter = new League\Flysystem\Ftp\FtpAdapter( + FtpConnectionOptions::fromArray([ + 'host' => 'hostname', + 'root' => '/root/path/', + 'username' => 'username', + 'password' => 'password', + 'port' => 21, + 'ssl' => false, + 'timeout' => 90, + 'utf8' => false, + 'passive' => true, + 'transferMode' => FTP_BINARY, + 'systemType' => null, // 'windows' or 'unix' + 'ignorePassiveAddress' => null, // true or false + 'timestampsOnUnixListingsEnabled' => false, // true or false + 'recurseManually' => true // true + ]) +); + +// The FilesystemOperator +$filesystem = new Filesystem($adapter); Dispatcher::run([ new Middlewares\Reader($filesystem) @@ -77,7 +89,7 @@ $reader = new Middlewares\Reader($filesystem, $responseFactory, $streamFactory); Allows to continue to the next middleware on error (file not found, method not allowed, etc). This allows to create a simple caching system as the following: ```php -$cache = new Flysystem(new Local(__DIR__.'/path/to/files')); +$cache = new Filesystem(new LocalFilesystemAdapter(__DIR__.'/path/to/files')); Dispatcher::run([ (new Middlewares\Reader($cache)) //read and returns the cached response... @@ -103,7 +115,7 @@ To be compatible with `Reader` behaviour: * If the response is gzipped (has the header `Content-Encoding: gzip`) the file is saved with the extension .gz. For example `/post/23/index.html.gz` (instead `/post/23/index.html`). ```php -$filesystem = new Flysystem(new Local(__DIR__.'/storage')); +$filesystem = new Filesystem(new LocalFilesystemAdapter(__DIR__.'/storage')); Dispatcher::run([ new Middlewares\Writer($filesystem) diff --git a/composer.json b/composer.json index 410c506..2c938de 100644 --- a/composer.json +++ b/composer.json @@ -18,17 +18,17 @@ }, "require": { "php": "^7.2 || ^8.0", - "middlewares/utils": "^3.0", - "league/flysystem": "^1.0", - "psr/http-server-middleware": "^1.0" + "middlewares/utils": "^2 || ^3 || ^4", + "league/flysystem": "^2 || ^3", + "psr/http-server-middleware": "^1" }, "require-dev": { - "phpunit/phpunit": "^8|^9", - "friendsofphp/php-cs-fixer": "^2.0", - "squizlabs/php_codesniffer": "^3.0", - "oscarotero/php-cs-fixer-config": "^1.0", - "phpstan/phpstan": "^0.12", - "laminas/laminas-diactoros": "^2.3" + "phpunit/phpunit": "^8 || ^9", + "friendsofphp/php-cs-fixer": "^3", + "squizlabs/php_codesniffer": "^3", + "oscarotero/php-cs-fixer-config": "^2", + "phpstan/phpstan": "^1 || ^2", + "laminas/laminas-diactoros": "^2 || ^3" }, "autoload": { "psr-4": { diff --git a/phpcs.xml.dist b/phpcs.xml similarity index 100% rename from phpcs.xml.dist rename to phpcs.xml diff --git a/phpunit.xml.dist b/phpunit.xml similarity index 100% rename from phpunit.xml.dist rename to phpunit.xml diff --git a/src/Filesystem.php b/src/Filesystem.php index 4be7841..26b5edd 100644 --- a/src/Filesystem.php +++ b/src/Filesystem.php @@ -3,20 +3,20 @@ namespace Middlewares; -use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem as Flysystem; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemOperator; +use League\Flysystem\Local\LocalFilesystemAdapter; abstract class Filesystem { /** - * @var FilesystemInterface + * @var FilesystemOperator */ protected $filesystem; - protected static function createLocalFlysystem(string $path): FilesystemInterface + protected static function createLocalFlysystem(string $path): FilesystemOperator { - return new Flysystem(new Local($path)); + return new Flysystem(new LocalFilesystemAdapter($path)); } /** @@ -25,8 +25,8 @@ protected static function createLocalFlysystem(string $path): FilesystemInterfac protected static function getFilename(string $path): string { $parts = pathinfo(urldecode($path)); - $path = isset($parts['dirname']) ? $parts['dirname'] : ''; - $filename = isset($parts['basename']) ? $parts['basename'] : ''; + $path = $parts['dirname'] ?? ''; + $filename = $parts['basename']; //if has not extension, assume it's a directory and append index.html if (empty($parts['extension'])) { diff --git a/src/Reader.php b/src/Reader.php index 2c9873f..ef9b280 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -3,7 +3,7 @@ namespace Middlewares; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemOperator; use Middlewares\Utils\Factory; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; @@ -32,16 +32,17 @@ class Reader extends Filesystem implements MiddlewareInterface public static function createFromDirectory( string $path, - ResponseFactoryInterface $responseFactory = null, - StreamFactoryInterface $streamFactory = null + ?ResponseFactoryInterface $responseFactory = null, + ?StreamFactoryInterface $streamFactory = null ): self { + /* @note We use static so that other classes can extend it and get the expected behaviour */ return new static(static::createLocalFlysystem($path), $responseFactory, $streamFactory); } public function __construct( - FilesystemInterface $filesystem, - ResponseFactoryInterface $responseFactory = null, - StreamFactoryInterface $streamFactory = null + FilesystemOperator $filesystem, + ?ResponseFactoryInterface $responseFactory = null, + ?StreamFactoryInterface $streamFactory = null ) { $this->filesystem = $filesystem; $this->responseFactory = $responseFactory ?: Factory::getResponseFactory(); @@ -74,14 +75,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $file = static::getFilename($request->getUri()->getPath()); - if ($this->filesystem->has($file)) { + if ($this->filesystem->fileExists($file)) { return $this->read($request, $file); } //If the file does not exists, check if is gzipped $file .= '.gz'; - if (stripos($request->getHeaderLine('Accept-Encoding'), 'gzip') === false || !$this->filesystem->has($file)) { + if (stripos($request->getHeaderLine('Accept-Encoding'), 'gzip') === false + || !$this->filesystem->fileExists($file) + ) { if ($this->continueOnError) { return $handler->handle($request); } @@ -97,6 +100,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface */ private function read(ServerRequestInterface $request, string $file): ResponseInterface { + /** @var resource|false $resource */ $resource = $this->filesystem->readStream($file); if ($resource === false) { @@ -136,7 +140,7 @@ private static function range(ResponseInterface $response, string $range): Respo /** * Parses a range header, for example: bytes=500-999. * - * @return false|array [first, last] + * @return array{0: int, 1: int|null}|false */ private static function parseRangeHeader(string $header) { diff --git a/src/Writer.php b/src/Writer.php index 39148d6..0dcff6e 100644 --- a/src/Writer.php +++ b/src/Writer.php @@ -3,7 +3,7 @@ namespace Middlewares; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemOperator; use Middlewares\Utils\Factory; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -21,14 +21,15 @@ class Writer extends Filesystem implements MiddlewareInterface public static function createFromDirectory( string $path, - StreamFactoryInterface $streamFactory = null + ?StreamFactoryInterface $streamFactory = null ): self { + /* @note We use static so that other classes can extend it and get the expected behaviour */ return new static(static::createLocalFlysystem($path), $streamFactory); } public function __construct( - FilesystemInterface $filesystem, - StreamFactoryInterface $streamFactory = null + FilesystemOperator $filesystem, + ?StreamFactoryInterface $streamFactory = null ) { $this->filesystem = $filesystem; $this->streamFactory = $streamFactory ?: Factory::getStreamFactory(); diff --git a/tests/ReaderTest.php b/tests/ReaderTest.php index c42848a..345ebb5 100644 --- a/tests/ReaderTest.php +++ b/tests/ReaderTest.php @@ -1,5 +1,7 @@ getHeaderLine('Allow')); } - public function testGz() + public function testGz(): void { $response = Dispatcher::run( [ @@ -49,7 +52,7 @@ public function testGz() self::assertEquals('gzip', $response->getHeaderLine('Content-Encoding')); } - public function testNotFound() + public function testNotFound(): void { $response = Dispatcher::run( [ @@ -61,7 +64,7 @@ public function testNotFound() self::assertEquals(404, $response->getStatusCode()); } - public function testContinueOnNotFound() + public function testContinueOnNotFound(): void { $response = Dispatcher::run( [ @@ -79,7 +82,7 @@ function () { self::assertEquals('Fallback', (string) $response->getBody()); } - public function testContinueOnInvalidMethod() + public function testContinueOnInvalidMethod(): void { $response = Dispatcher::run( [ @@ -97,7 +100,7 @@ function () { self::assertEquals('Fallback', (string) $response->getBody()); } - public function testIndex() + public function testIndex(): void { $response = Dispatcher::run( [ @@ -112,7 +115,7 @@ public function testIndex() self::assertEquals($content, (string) $response->getBody()); } - public function testContentRange() + public function testContentRange(): void { $response = Dispatcher::run( [ @@ -126,7 +129,7 @@ public function testContentRange() self::assertMatchesRegularExpression('|^bytes 300-\d{6}/\d{6}$|', $response->getHeaderLine('Content-Range')); } - public function testInvalidContentRange() + public function testInvalidContentRange(): void { $response = Dispatcher::run( [ diff --git a/tests/WriterTest.php b/tests/WriterTest.php index 0d56365..6ae8c7c 100644 --- a/tests/WriterTest.php +++ b/tests/WriterTest.php @@ -1,5 +1,7 @@ Date: Fri, 21 Mar 2025 13:09:23 +0100 Subject: [PATCH 2/4] support for PHP 8.4 amongst other improvements --- .phpstan.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.phpstan.neon b/.phpstan.neon index a3d1fa1..800d4e9 100644 --- a/.phpstan.neon +++ b/.phpstan.neon @@ -1,7 +1,7 @@ parameters: reportUnmatchedIgnoredErrors: false inferPrivatePropertyTypeFromConstructor: true - level: max + level: 8 paths: - src - tests From 30fca8668ec5938dc90ee23226dd2d02e98cf5ae Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Fri, 21 Mar 2025 16:50:09 +0100 Subject: [PATCH 3/4] workflow updated --- .github/workflows/main.yaml | 74 ++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e7d07a0..5d14a59 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1 +1,73 @@ -/src/config/workflow.yaml \ No newline at end of file +name: "testing" + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + qa: + name: Quality assurance + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress + + - name: Coding Standard + run: composer run-script cs + + tests: + name: Tests + runs-on: ubuntu-latest + + strategy: + matrix: + php: + - 7.2 + - 7.3 + - 7.4 + - 8.0 + - 8.1 + - 8.2 + - 8.3 + - 8.4 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Cache PHP dependencies + uses: actions/cache@v4 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Tests + run: composer test From c22f0a87aca475c69c9cc9808da8135fa93df4ef Mon Sep 17 00:00:00 2001 From: Filis Futsarov Date: Fri, 21 Mar 2025 17:06:48 +0100 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36600da..0fde140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [3.0.0] - 2020-12-03 ### Changed +- PHP 7.2 remains th minimum version. - Updated `league/filesystem` to version 2 for php 7.2, 7.3 and 7.4, and to version 3 for >= php 8.0. - With these changes, a breaking change is introduced by `league/filesystem`. Now you have to pass a FilesystemOperator interface compliant class to the middlewares. Check README's code examples.