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 @@
+