diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..480bd08 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: Package CI + +on: + pull_request: + +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + strategy: + matrix: + php: [ 7.4, 8.0 ] + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - run: make composer + + - if: matrix.php == '8.0' + run: make cs + + - if: matrix.php == '8.0' + run: make phpstan + + - run: make run-tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5c3db2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/composer.lock +/vendor +/tests/**/*.actual +/tests/**/*.expected +/tests/temp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa09bf8 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +composer: + composer validate + composer update --no-interaction --prefer-dist + +phpstan: + vendor/bin/phpstan analyse src/ -c phpstan.neon --level 8 --no-progress + +cs: + vendor/bin/phpcs src/ --standard=vendor/pd/coding-standard/src/PeckaCodingStandard/ruleset.xml + vendor/bin/phpcs src/ --standard=vendor/pd/coding-standard/src/PeckaCodingStandardStrict/ruleset.xml + +run-tests: + vendor/bin/tester -C tests/ diff --git a/composer.json b/composer.json index f4153f1..4e84052 100644 --- a/composer.json +++ b/composer.json @@ -1,21 +1,40 @@ { - "name": "vitkutny/version", + "name": "pd/version", + "description": "Filtr pro zjisštění verze obsahu URL", "extra": { "extensions": { - "vitkutny.version": "VitKutny\\Version\\Extension" + "pd.version": "Pd\\Version\\Extension" } }, "require": { - "php": "~7.0", - "nette/http": "~2.0" + "php": "^7.4 | ^8.0", + "nette/http": "^3.0", + "nette/caching": "^3.0", + "nette/application": "^3.0", + "symfony/console": "^3.0 | ^4.0 | ^5.0" + }, + "require-dev": { + "pd/coding-standard": "v1.27.2", + "nette/tester": "^2.4", + "nette/bootstrap": "^3.0", + "phpstan/phpstan": "0.12.88", + "phpstan/phpstan-strict-rules": "0.12.9", + "latte/latte": "^2.9" }, "suggest": { "nette/caching": "Caching support", - "nette/di": "Register filter using extension" + "nette/di": "Register filter using extension", + "kdyby/console": "For Symfony Console support", + "contributte/console": "For Symfony Console support" }, "autoload": { "psr-4": { - "VitKutny\\": "src/" + "Pd\\Version\\": "src/" } + }, + "autoload-dev": { + "classmap": [ + "tests/" + ] } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..542cbdf --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + message: '~^Parameter #2 \$(format|associative) of function get_headers expects (int|bool), int\|true given\.$~' + path: src/Resolvers/AbsoluteUrlResolver.php diff --git a/src/CleanCacheCommand.php b/src/CleanCacheCommand.php new file mode 100644 index 0000000..abbd7e0 --- /dev/null +++ b/src/CleanCacheCommand.php @@ -0,0 +1,40 @@ +setName('pd:version:clean-cache'); + $this->setDescription('Smaže cache verzí'); + } + + + protected function execute( + \Symfony\Component\Console\Input\InputInterface $input, + \Symfony\Component\Console\Output\OutputInterface $output + ) { + /** @var \Nette\Caching\IStorage $storage */ + $storage = $this->getHelper('container')->getByType(\Nette\Caching\IStorage::class); + $conditions = [ + 'tags' => [ + Filter::CACHE_TAG, + ], + ]; + + if ($output->getVerbosity() > \Symfony\Component\Console\Output\Output::VERBOSITY_QUIET) { + $output->write('Smaže se cache verzí: '); + } + + $storage->clean($conditions); + + if ($output->getVerbosity() > \Symfony\Component\Console\Output\Output::VERBOSITY_QUIET) { + $output->writeln('Smazáno'); + } + + return 0; + } + +} diff --git a/src/Extension.php b/src/Extension.php new file mode 100644 index 0000000..40d24eb --- /dev/null +++ b/src/Extension.php @@ -0,0 +1,105 @@ +directory = $directory; + $this->parameter = $parameter; + $this->debugMode = $debugMode; + } + + + public function beforeCompile(): void + { + $builder = $this->getContainerBuilder(); + /** @var \Nette\DI\Definitions\ServiceDefinition $filter */ + $filter = $builder->getDefinition($this->prefix('filter')); + /** @var \Nette\DI\Definitions\ServiceDefinition $absoluteUrlResolver */ + $absoluteUrlResolver = $builder->getDefinition($this->prefix('absoluteUrlResolver')); + /** @var \Nette\DI\Definitions\ServiceDefinition $pathResolver */ + $pathResolver = $builder->getDefinition($this->prefix('pathResolver')); + + $request = $builder->getByType(\Nette\Http\IRequest::class); + if ($request) { + $absoluteUrlResolver->addSetup('setRequest', [$builder->getDefinition($request)]); + $pathResolver->addSetup('setRequest', [$builder->getDefinition($request)]); + } + + $storage = $builder->getByType(\Nette\Caching\IStorage::class); + if ($storage) { + $absoluteUrlResolver->addSetup('setStorage', [ + $builder->getDefinition($storage), + ]); + $pathResolver->addSetup('setStorage', [ + $builder->getDefinition($storage), + ]); + } + + $engine = $builder->getByType(\Nette\Bridges\ApplicationLatte\ILatteFactory::class); + if ($engine) { + /** @var \Nette\DI\Definitions\FactoryDefinition $latteEngine */ + $latteEngine = $builder->getDefinition($engine); + $latteEngine->getResultDefinition()->addSetup('addFilter', [ + 'version', + $filter, + ]) + ; + } + } + + + public function loadConfiguration(): void + { + $builder = $this->getContainerBuilder(); + + $absoluteUrlResolver = $builder->addDefinition($this->prefix('absoluteUrlResolver')) + ->setFactory(\Pd\Version\Resolvers\AbsoluteUrlResolver::class) + ; + + $pathResolver = $builder->addDefinition($this->prefix('pathResolver')) + ->setFactory(\Pd\Version\Resolvers\PathResolver::class) + ->setArguments([$this->debugMode]) + ; + + $arguments = [ + $this->directory, + $this->parameter, + $absoluteUrlResolver, + $pathResolver, + ]; + $builder + ->addDefinition($this->prefix('filter')) + ->setFactory(Filter::class) + ->setArguments($arguments) + ; + + $builder->addDefinition($this->prefix('relativePathGetter')) + ->setFactory(\Pd\Version\Resolvers\Getter\RelativePathGetter::class) + ; + + if (\PHP_SAPI !== 'cli') { + return; + } + $builder = $this->getContainerBuilder(); + $cleanCacheDefinition = $builder + ->addDefinition($this->prefix('console.cleanCache')) + ->setFactory(CleanCacheCommand::class) + ; + + if (\class_exists('\Kdyby\Console\DI\ConsoleExtension')) { + $cleanCacheDefinition->addTag('kdyby.console.command'); + } + } + +} diff --git a/src/Filter.php b/src/Filter.php new file mode 100644 index 0000000..e0c36a1 --- /dev/null +++ b/src/Filter.php @@ -0,0 +1,56 @@ +directory = $directory; + $this->parameter = $parameter; + $this->getters = $getters; + } + + + /** + * @param string|\Nette\Http\Url|\Nette\Http\UrlImmutable $url + */ + public function __invoke( + $url, + ?string $directory = NULL, + ?string $parameter = NULL + ): ?string + { + $directory = $directory ?: $this->directory; + $parameter = $parameter ?: $this->parameter; + + $url = new \Nette\Http\Url($url); + foreach ($this->getters as $getter) { + $filePath = $getter->resolve($url, $directory, $parameter); + if ($filePath) { + return $filePath; + } + } + + return NULL; + } + +} diff --git a/src/IFilter.php b/src/IFilter.php new file mode 100644 index 0000000..5a60d44 --- /dev/null +++ b/src/IFilter.php @@ -0,0 +1,17 @@ +isAbsoluteUrl($url)) { + return NULL; + } + + if ( ! $this->cache) { + return $this->process($url, $parameter); + } + + return $this->cache->load([$url->path, $directory, $parameter], function () use ($url, $parameter): string { + return $this->process($url, $parameter); + }); + } + + + private function process(\Nette\Http\Url $url, string $parameter): string + { + $version = ''; + $headers = @\get_headers($url->getAbsoluteUrl(), \PHP_VERSION_ID >= 80000 ? TRUE : 1); + if (\is_array($headers) && isset($headers['ETag'])) { + $version = \preg_replace('~[^a-z0-9\-]~', '', $headers['ETag']); + } elseif (\is_array($headers) && isset($headers['Last-Modified'])) { + $version = (string) (new \DateTime($headers['Last-Modified']))->getTimestamp(); + } + + return $this->getPath($url, $version, $parameter); + } + + + private function isAbsoluteUrl(\Nette\Http\Url $url): bool + { + if ( ! \Nette\Utils\Strings::length($url->getHost())) { + return FALSE; + } + + return $this->request && $url->getHost() !== $this->request->getUrl()->getHost(); + } + +} diff --git a/src/Resolvers/AbstractPathResolver.php b/src/Resolvers/AbstractPathResolver.php new file mode 100644 index 0000000..7079e31 --- /dev/null +++ b/src/Resolvers/AbstractPathResolver.php @@ -0,0 +1,35 @@ +request = $request; + } + + + public function setStorage(\Nette\Caching\IStorage $storage): void + { + $this->cache = new \Nette\Caching\Cache($storage, \strtr(self::class, '\\', self::NAMESPACE_SEPARATOR)); + } + + + protected function getPath(\Nette\Http\Url $url, string $version, string $parameter): string + { + $url->setQueryParameter($parameter, $version ?: \time()); + + return (string) \preg_replace($pattern = '#^(\\+|/+)#', \preg_match($pattern, $url->getPath()) ? \DIRECTORY_SEPARATOR : '', $url->getAbsoluteUrl()); + } + +} diff --git a/src/Resolvers/Getter/RelativePathGetter.php b/src/Resolvers/Getter/RelativePathGetter.php new file mode 100644 index 0000000..e5d8b46 --- /dev/null +++ b/src/Resolvers/Getter/RelativePathGetter.php @@ -0,0 +1,22 @@ +debugMode = $debugMode; + $this->relativePathGetter = $relativePathGetter; + } + + + public function resolve(\Nette\Http\Url $url, string $directory, string $parameter): ?string + { + $realPath = $this->relativePathGetter->getFileName($directory, $url->getPath()); + + if ( ! $realPath) { + return NULL; + } + + if ( ! $this->cache) { + return $this->process($url, $realPath, $parameter); + } + + return $this->cache->load([$realPath, $directory, $parameter], function (?array &$dependencies) use ($url, $realPath, $parameter): string { + return $this->process($url, $realPath, $parameter, $dependencies); + }); + } + + + /** + * @param array $dependencies + */ + private function process(\Nette\Http\Url $url, string $realPath, string $parameter, ?array &$dependencies = NULL): string + { + $version = (string) \sha1_file($realPath); + if ($this->debugMode) { + $dependencies[\Nette\Caching\Cache::FILES] = $realPath; + } + $dependencies[\Nette\Caching\Cache::TAGS] = [\Pd\Version\Filter::CACHE_TAG]; + + return $this->getPath($url, $version, $parameter); + } + +} diff --git a/src/Resolvers/PathResolverInterface.php b/src/Resolvers/PathResolverInterface.php new file mode 100644 index 0000000..624a0ca --- /dev/null +++ b/src/Resolvers/PathResolverInterface.php @@ -0,0 +1,13 @@ + '%wwwDir%', - 'parameter' => 'version', - 'expire' => '+1 hour', - ]; - - public function beforeCompile() - { - parent::beforeCompile(); - $builder = $this->getContainerBuilder(); - $filter = $builder->getDefinition($this->prefix('filter')); - if ($request = $builder->getByType(Nette\Http\IRequest::class)) { - $filter->addSetup('setRequest', [$builder->getDefinition($request)]); - } - if ($storage = $builder->getByType(Nette\Caching\IStorage::class)) { - $filter->addSetup('setStorage', [ - $builder->getDefinition($storage), - $this->config['expire'], - ]); - } - if ($engine = $builder->getByType(Nette\Bridges\ApplicationLatte\ILatteFactory::class)) { - $builder->getDefinition($engine)->addSetup('addFilter', [ - 'version', - $filter, - ]); - } - } - - public function loadConfiguration() - { - parent::loadConfiguration(); - $this->config = $this->getConfig($this->defaults); - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('filter'))->setClass(Filter::class)->setArguments([ - $this->config['directory'], - $this->config['parameter'], - ]); - } -} diff --git a/src/Version/Filter.php b/src/Version/Filter.php deleted file mode 100644 index 511ebbf..0000000 --- a/src/Version/Filter.php +++ /dev/null @@ -1,110 +0,0 @@ -directory = $directory; - $this->parameter = $parameter; - } - - public function setRequest(Nette\Http\IRequest $request) - { - $this->request = $request; - } - - public function setStorage( - Nette\Caching\IStorage $storage, - $expire - ) { - $this->cache = new Nette\Caching\Cache($storage, strtr(self::class, '\\', Nette\Caching\Cache::NAMESPACE_SEPARATOR)); - $this->expire = $expire instanceof DateTime ? $expire : new DateTime($expire); - } - - public function __invoke( - $url, - string $directory = NULL, - string $parameter = NULL - ) : string - { - $arguments = [ - $url, - $directory ? : $this->directory, - $parameter ? : $this->parameter, - ]; - - return $this->cache ? $this->cache->load($arguments, function (& $dependencies) use - ( - $arguments - ) { - $dependencies[Nette\Caching\Cache::EXPIRE] = $this->expire; - $arguments[] = &$dependencies; - - return $this->process(...$arguments); - }) : $this->process(...$arguments); - } - - private function process( - $url, - string $directory, - string $parameter, - array & $dependencies = [] - ) : string - { - $url = new Nette\Http\Url($url); - $time = NULL; - if ($url->getHost() && ( ! $this->request || $url->getHost() !== $this->request->getUrl()->getHost())) { - $headers = @get_headers($url, TRUE); - if (is_array($headers) && isset($headers['Last-Modified'])) { - $time = (new DateTime($headers['Last-Modified']))->getTimestamp(); - } - } elseif (is_file($filename = implode(DIRECTORY_SEPARATOR, [ - rtrim($directory, '\\/'), - ltrim($url->getPath(), '\\/'), - ]))) { - $time = filemtime($filename); - unset($dependencies[Nette\Caching\Cache::EXPIRE]); - $dependencies[Nette\Caching\Cache::FILES] = $filename; - } - $url->setQueryParameter($parameter, $time ? : ($this->time ? : $this->time = time())); - - return preg_replace($pattern = '#^(\\+|/+)#', preg_match($pattern, $url->getPath()) ? DIRECTORY_SEPARATOR : NULL, $url); - } -} diff --git a/tests/PdTests/Version/Cache/TagsTest.php b/tests/PdTests/Version/Cache/TagsTest.php new file mode 100644 index 0000000..335d08c --- /dev/null +++ b/tests/PdTests/Version/Cache/TagsTest.php @@ -0,0 +1,28 @@ +container->getByType(\PdTests\Version\Cache\TestDevNullStorage::class); + + /** @var \Pd\Version\Resolvers\PathResolver $resolver */ + $resolver = $this->container->getByType(\Pd\Version\Resolvers\PathResolver::class); + $resolver->resolve(new \Nette\Http\Url('test.txt'), __DIR__ . '/files', ''); + + \Tester\Assert::true(isset($storage->getDependencies()[\Nette\Caching\Cache::TAGS])); + + $defaultTag = \current($storage->getDependencies()[\Nette\Caching\Cache::TAGS]); + + \Tester\Assert::same($defaultTag, \Pd\Version\Filter::CACHE_TAG); + } + +} +(new TagsTest())->run(); diff --git a/tests/PdTests/Version/Cache/TestDevNullStorage.php b/tests/PdTests/Version/Cache/TestDevNullStorage.php new file mode 100644 index 0000000..eb2ccc6 --- /dev/null +++ b/tests/PdTests/Version/Cache/TestDevNullStorage.php @@ -0,0 +1,27 @@ +dependencies = $dependencies; + + parent::write($key,$data, $dependencies); + } + + + public function getDependencies(): array + { + return $this->dependencies; + } + +} diff --git a/tests/PdTests/Version/Cache/files/test.txt b/tests/PdTests/Version/Cache/files/test.txt new file mode 100644 index 0000000..1daa932 --- /dev/null +++ b/tests/PdTests/Version/Cache/files/test.txt @@ -0,0 +1 @@ +Tohle je test cache diff --git a/tests/PdTests/Version/DI/ExtensionTest.php b/tests/PdTests/Version/DI/ExtensionTest.php new file mode 100644 index 0000000..8569d21 --- /dev/null +++ b/tests/PdTests/Version/DI/ExtensionTest.php @@ -0,0 +1,20 @@ +container->getByType(\Pd\Version\Filter::class); + \Tester\Assert::type(\Pd\Version\Filter::class, $service); + + $service = $this->container->getByType(\Pd\Version\CleanCacheCommand::class); + \Tester\Assert::type(\Pd\Version\CleanCacheCommand::class, $service); + } +} +(new ExtensionTest())->run(); diff --git a/tests/PdTests/Version/DI/extension.neon b/tests/PdTests/Version/DI/extension.neon new file mode 100644 index 0000000..21e8a6b --- /dev/null +++ b/tests/PdTests/Version/DI/extension.neon @@ -0,0 +1,5 @@ +extensions: + version: Pd\Version\Extension + +services: + cache.storage: PdTests\Version\Cache\TestDevNullStorage diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..dcbc425 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,26 @@ +addConfig(__DIR__ . '/PdTests/Version/DI/extension.neon'); + $configurator->setTempDirectory(__DIR__ . '/temp'); + + $this->container = $configurator->createContainer(); + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..1241910 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +