diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a142b6..d3278ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.1', '8.2'] + php: ['8.1', '8.2', '8.3'] name: Testing on PHP ${{ matrix.php }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -27,4 +27,7 @@ jobs: run: vendor/bin/phpunit - name: Run phpstan - run: vendor/bin/phpstan analyze --no-progress --level=5 src/ + run: vendor/bin/phpstan + + - name: Validate coding standards + run: vendor/bin/phpcs diff --git a/Dockerfile b/Dockerfile index b319775..e169d89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,17 @@ -FROM php:8-cli +FROM php:8.1-cli + +# install dependencies +RUN apt update \ + && apt install -y \ + libicu-dev \ + git \ + zip \ + && pecl install xdebug \ + && docker-php-ext-enable \ + xdebug \ + && docker-php-ext-install \ + intl \ + && apt-get clean # install composer -# RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/README.md b/README.md index 191cf27..36d37cb 100755 --- a/README.md +++ b/README.md @@ -26,12 +26,18 @@ Here are some code samples, to show how the library is handled. use Intervention\MimeSniffer\MimeSniffer; use Intervention\MimeSniffer\Types\ImageJpeg; -// detect given string +// universal factory method +$sniffer = MimeSniffer::create($content); + +// or detect given string $sniffer = MimeSniffer::createFromString($content); // or detect given file $sniffer = MimeSniffer::createFromFilename('image.jpg'); +// or detect from file pointer +$sniffer = MimeSniffer::createFromFilename(fopen('test.jpg', 'r')); + // returns object of detected type $type = $sniffer->getType(); @@ -65,6 +71,9 @@ $type = $sniffer->setFromString($other_content)->getType(); // or with setter for filename $type = $sniffer->setFromFilename('images/image.jpg')->getType(); + +// or with setter for file pointer +$type = $sniffer->setFromPointer(fopen('images/image.jpg', 'r'))->getType(); ``` **Currently only the following file types can be detected. More will be added in a next release.** diff --git a/composer.json b/composer.json index 5841224..4ac9ab3 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,13 @@ } ], "require": { - "php": "^7.3|^8.0" + "php": "^8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", - "phpstan/phpstan": "^1" + "phpstan/phpstan": "^1", + "squizlabs/php_codesniffer": "^3.8", + "slevomat/coding-standard": "~8.0" }, "autoload": { "psr-4": { @@ -30,8 +32,13 @@ }, "autoload-dev": { "psr-4": { - "Intervention\\MimeSniffer\\Test\\": "tests" + "Intervention\\MimeSniffer\\Tests\\": "tests" } }, - "minimum-stability": "stable" + "minimum-stability": "stable", + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } } diff --git a/docker-compose.yml b/docker-compose.yml index 4cca254..648a1aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,12 @@ services: analysis: build: ./ working_dir: /project - command: bash -c "composer install && ./vendor/bin/phpstan analyze --level=4 ./src" + command: bash -c "composer install && ./vendor/bin/phpstan analyze ./src" + volumes: + - ./:/project + standards: + build: ./ + working_dir: /project + command: bash -c "composer install && ./vendor/bin/phpcs" volumes: - ./:/project diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..226f59a --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,68 @@ + + + src/ + tests/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpstan.dist.neon b/phpstan.dist.neon new file mode 100644 index 0000000..906ac9e --- /dev/null +++ b/phpstan.dist.neon @@ -0,0 +1,9 @@ +parameters: + level: 6 + paths: + - src + exceptions: + check: + missingCheckedExceptionInThrows: true + uncheckedExceptionClasses: + - Error diff --git a/src/AbstractBinaryType.php b/src/AbstractBinaryType.php index 3cad5d5..8b72e0f 100644 --- a/src/AbstractBinaryType.php +++ b/src/AbstractBinaryType.php @@ -1,5 +1,7 @@ setFromString($content); + if (is_string($content) && file_exists($content)) { + $this->setFromFilename($content); + } + + if (is_string($content)) { + $this->setFromString($content); + } + + if (is_resource($content)) { + $this->setFromPointer($content); + } + } + + /** + * Universal factory method + * + * @param mixed $content + * @throws InvalidArgumentException + * @return MimeSniffer + */ + public static function create(mixed $content): self + { + return new self($content); } /** * Create new instance from given string * * @param string $content - * + * @throws InvalidArgumentException * @return MimeSniffer */ - public static function createFromString(string $content): MimeSniffer + public static function createFromString(string $content): self { - return new self($content); + return (new self())->setFromString($content); } /** * Load contents of given string into instance * * @param string $content - * + * @throws InvalidArgumentException * @return MimeSniffer */ - public function setFromString(string $content): MimeSniffer + public function setFromString(string $content): self { - $this->content = strval($content); + $this->content = $content; return $this; } @@ -51,10 +80,10 @@ public function setFromString(string $content): MimeSniffer * Create a new instance and load contents of given filename * * @param string $filename - * + * @throws InvalidArgumentException * @return MimeSniffer */ - public static function createFromFilename(string $filename): MimeSniffer + public static function createFromFilename(string $filename): self { return (new self())->setFromFilename($filename); } @@ -63,10 +92,10 @@ public static function createFromFilename(string $filename): MimeSniffer * Load contents of given filename in current instance * * @param string $filename - * + * @throws InvalidArgumentException * @return MimeSniffer */ - public function setFromFilename(string $filename): MimeSniffer + public function setFromFilename(string $filename): self { $fp = fopen($filename, 'r'); $this->setFromString(fread($fp, 1024)); @@ -75,12 +104,42 @@ public function setFromFilename(string $filename): MimeSniffer return $this; } + /** + * Create a new instance and load contents of given filename + * + * @param resource $pointer + * @throws InvalidArgumentException + * @return MimeSniffer + */ + public static function createFromPointer($pointer): self + { + return (new self())->setFromPointer($pointer); + } + + /** + * Load contents of given filename in current instance + * + * @throws InvalidArgumentException + * @param resource $pointer + * @return MimeSniffer + */ + public function setFromPointer($pointer): self + { + if (!is_resource($pointer)) { + throw new InvalidArgumentException('Argument #1 $pointer must be of type resource.'); + } + + $this->setFromString(fread($pointer, 1024)); + + return $this; + } + /** * Return detected type * - * @return AbstractType + * @return TypeInterface */ - public function getType(): AbstractType + public function getType(): TypeInterface { foreach ($this->getTypeClassnames() as $classname) { $type = new $classname(); @@ -98,30 +157,34 @@ public function getType(): AbstractType } /** - * Determine if content matches the given type - * or any if the given types in array + * Determine if content matches the given type or any if the given types in array * - * @param AbstractType|array $types AbstractType or array of AbstractTypes - * @return boolean + * @param TypeInterface|string|array $types + * @return bool */ - public function matches($types): bool + public function matches(TypeInterface|string|array $types): bool { - if (! is_array($types)) { + if (!is_array($types)) { $types = [$types]; } - $types = array_map(function ($value) { - if (is_a($value, AbstractType::class)) { - return $value; - } + $types = array_filter($types, function ($type) { + return match (true) { + ($type instanceof TypeInterface) => true, + is_string($type) && class_exists($type) => true, + default => false, + }; + }); - if (!is_null($value) && class_exists($value)) { - return new $value(); - } + $types = array_map(function ($type) { + return match (true) { + is_string($type) => new $type(), + default => $type, + }; }, $types); $types = array_filter($types, function ($type) { - return is_a($type, AbstractType::class); + return $type instanceof TypeInterface; }); foreach ($types as $type) { @@ -136,7 +199,7 @@ public function matches($types): bool /** * Return array of type classnames * - * @return array + * @return array */ private function getTypeClassnames(): array { @@ -147,7 +210,7 @@ private function getTypeClassnames(): array }, $files); return array_filter($classnames, function ($classname) { - return ! in_array($classname, [ + return !in_array($classname, [ Types\ApplicationOctetStream::class, Types\TextPlain::class, ]); diff --git a/src/Types/ApplicationGzip.php b/src/Types/ApplicationGzip.php index 4a0a103..6ae2423 100644 --- a/src/Types/ApplicationGzip.php +++ b/src/Types/ApplicationGzip.php @@ -1,5 +1,7 @@ getMockForAbstractClass(AbstractType::class); - $this->assertEquals('', $type); + $this->assertEquals('', (string) $this->getTestAbstractType()); } public function testMatches(): void { - $type = $this->getMockForAbstractClass(AbstractType::class); - $this->assertFalse($type->matches('test')); + $this->assertFalse($this->getTestAbstractType()->matches('test')); } public function testIsImage(): void { - $type = $this->getMockForAbstractClass(AbstractType::class); - $this->assertFalse($type->isImage()); + $this->assertFalse($this->getTestAbstractType()->isImage()); } public function testPrepareContent(): void @@ -32,13 +31,18 @@ public function testPrepareContent(): void $content .= 'x'; } - $type = $this->getMockForAbstractClass(AbstractType::class); - $this->assertEquals(1024, strlen($type->prepareContent($content))); + $this->assertEquals(1024, strlen($this->getTestAbstractType()->prepareContent($content))); } public function testIsBinary(): void { - $type = $this->getMockForAbstractClass(AbstractType::class); - $this->assertFalse($type->isBinary()); + $this->assertFalse($this->getTestAbstractType()->isBinary()); + } + + private function getTestAbstractType(): AbstractType + { + return new class () extends AbstractType + { + }; } } diff --git a/tests/ApplicationGzipTest.php b/tests/ApplicationGzipTest.php index e99e434..b57020e 100644 --- a/tests/ApplicationGzipTest.php +++ b/tests/ApplicationGzipTest.php @@ -1,6 +1,8 @@ assertInstanceOf(MimeSniffer::class, $sniffer); - $sniffer = new MimeSniffer(); + $sniffer = new MimeSniffer( + __DIR__ . '/../tests/files/test.jpg', + ); $this->assertInstanceOf(MimeSniffer::class, $sniffer); + + $sniffer = new MimeSniffer( + fopen(__DIR__ . '/../tests/files/test.jpg', 'r') + ); + $this->assertInstanceOf(MimeSniffer::class, $sniffer); + + $sniffer = new MimeSniffer( + 'foo' + ); + $this->assertInstanceOf(MimeSniffer::class, $sniffer); + } + + public function testCreate(): void + { + $this->assertInstanceOf(MimeSniffer::class, MimeSniffer::create( + __DIR__ . '/../tests/files/test.jpg', + )); + + $this->assertInstanceOf(MimeSniffer::class, MimeSniffer::create( + 'foo' + )); + + $this->assertInstanceOf(MimeSniffer::class, MimeSniffer::create( + fopen(__DIR__ . '/../tests/files/test.jpg', 'r'), + )); + + $this->assertInstanceOf(MimeSniffer::class, MimeSniffer::create( + null + )); } public function testCreateFromString(): void @@ -34,6 +66,12 @@ public function testCreateFromFilename(): void $this->assertInstanceOf(MimeSniffer::class, $sniffer); } + public function testCreateFromPointer(): void + { + $sniffer = MimeSniffer::createFromPointer(fopen(__DIR__ . '/../tests/files/test.jpg', 'r')); + $this->assertInstanceOf(MimeSniffer::class, $sniffer); + } + public function testSetFromString(): void { $sniffer = new MimeSniffer(); @@ -48,6 +86,13 @@ public function testSetFromFilename(): void $this->assertInstanceOf(MimeSniffer::class, $sniffer); } + public function testSetFromPointer(): void + { + $sniffer = new MimeSniffer(); + $sniffer = $sniffer->setFromPointer(fopen(__DIR__ . '/../tests/stubs/zip', 'r')); + $this->assertInstanceOf(MimeSniffer::class, $sniffer); + } + public function testMatchesType(): void { $sniffer = MimeSniffer::createFromFilename(__DIR__ . '/../tests/files/test.gif'); @@ -62,6 +107,10 @@ public function testMatchesArray(): void $this->assertFalse($sniffer->matches([new ImageJpeg(), new ImagePng()])); $this->assertTrue($sniffer->matches([ImageJpeg::class, ImagePng::class, ImageGif::class])); $this->assertFalse($sniffer->matches([ImageJpeg::class, ImagePng::class])); + $this->assertFalse($sniffer->matches([ImageJpeg::class, new ImagePng()])); + $this->assertFalse($sniffer->matches(['foo', new stdClass()])); + $this->assertTrue($sniffer->matches(['foo', new stdClass(), new ImageGif()])); + $this->assertTrue($sniffer->matches(['foo', new stdClass(), ImageGif::class])); } public function testMatchesBogus(): void @@ -70,7 +119,6 @@ public function testMatchesBogus(): void $this->assertFalse($sniffer->matches('foo')); $this->assertFalse($sniffer->matches(['foo', 'bar'])); $this->assertFalse($sniffer->matches([])); - $this->assertFalse($sniffer->matches(null)); $this->assertTrue($sniffer->matches(['foo', ApplicationZip::class])); } } diff --git a/tests/VideoMpegTest.php b/tests/VideoMpegTest.php index 4a14e26..00722fd 100644 --- a/tests/VideoMpegTest.php +++ b/tests/VideoMpegTest.php @@ -1,6 +1,8 @@