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..5d14a59 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,10 +1,10 @@ name: "testing" on: - push: - branches: [ master ] - pull_request: - branches: [ master ] + push: + branches: [ master ] + pull_request: + branches: [ master ] jobs: qa: @@ -13,19 +13,19 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Validate composer.json and composer.lock run: composer validate - name: Cache Composer packages id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + 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' @@ -40,19 +40,19 @@ jobs: strategy: matrix: - php: - - 7.2 - - 7.3 - - 7.4 - composer-args: [ "" ] - include: - - php: 8.0 - composer-args: --ignore-platform-reqs - fail-fast: false + php: + - 7.2 + - 7.3 + - 7.4 + - 8.0 + - 8.1 + - 8.2 + - 8.3 + - 8.4 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -60,17 +60,14 @@ jobs: php-version: ${{ matrix.php }} - name: Cache PHP dependencies - uses: actions/cache@v2 + 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 ${{ matrix.composer-args }} + run: composer install --prefer-dist --no-progress - name: Tests run: composer test - - - name: Tests coverage - run: composer coverage 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..800d4e9 --- /dev/null +++ b/.phpstan.neon @@ -0,0 +1,12 @@ +parameters: + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + level: 8 + paths: + - src + - tests + ignoreErrors: + - + identifier: 'function.alreadyNarrowedType' + reportUnmatched: false + - "#^Unsafe usage of new static#" diff --git a/CHANGELOG.md b/CHANGELOG.md index ba86514..0fde140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 +- 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. + ## [2.0.1] - 2020-12-03 ### Added - Support for PHP 8.0 @@ -69,6 +75,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 @@