Skip to content

Commit 3149cca

Browse files
authored
Merge pull request #1 from Incenteev/cache
Implement caching of the asset hashes for prod
2 parents 192fbcf + cedf958 commit 3149cca

21 files changed

+406
-63
lines changed

.travis.yml

+1-4
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ matrix:
1111
fast_finish: true
1212
include:
1313
- php: 7.0
14-
env: COMPOSER_FLAGS="--prefer-lowest"SYMFONY_DEPRECATIONS_HELPER=weak
14+
env: COMPOSER_FLAGS="--prefer-lowest" SYMFONY_DEPRECATIONS_HELPER=weak
1515
- php: 7.1
1616
env: DEPENDENCIES=dev SYMFONY_DEPRECATIONS_HELPER=weak
17-
- php: 7.0
18-
env: SYMFONY_VERSION=2.8.*
1917
allow_failures:
2018
- php: nightly
2119

@@ -24,7 +22,6 @@ cache:
2422
- $HOME/.composer/cache/files
2523

2624
before_install:
27-
- if [ "$SYMFONY_VERSION" != "" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --no-update; fi;
2825
- if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi;
2926

3027
install: composer update $COMPOSER_FLAGS

README.md

-15
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,6 @@ framework:
4343
version_strategy: incenteev_hashed_asset.strategy
4444
```
4545
46-
If you are using Symfony <3.1, this configuration setting is not available.
47-
Here is the workaround:
48-
49-
```yaml
50-
# app/config/config.yml
51-
framework:
52-
assets:
53-
version: dummy # set a dummy version so that the package does not use the empty version
54-
55-
services:
56-
assets._version__default:
57-
alias: incenteev_hashed_asset.strategy
58-
# If you use additional packages, you may need to create additional aliases for other packages than `_default`
59-
```
60-
6146
## Advanced configuration
6247
6348
The default configuration should fit common needs, but the bundle exposes

composer.json

+7-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
],
1212
"require": {
1313
"php": "^7.0",
14-
"symfony/asset": "^2.8 || ^3.0",
15-
"symfony/config": "^2.8 || ^3.0",
16-
"symfony/dependency-injection": "^2.8 || ^3.0",
17-
"symfony/http-kernel": "^2.8 || ^3.0"
14+
"symfony/asset": "^3.2",
15+
"symfony/cache": "^3.2",
16+
"symfony/config": "^3.2",
17+
"symfony/dependency-injection": "^3.2",
18+
"symfony/finder": "^3.2",
19+
"symfony/framework-bundle": "^3.2",
20+
"symfony/http-kernel": "^3.2"
1821
},
1922
"require-dev": {
2023
"symfony/phpunit-bridge": "^3.2"

src/Asset/HashingVersionStrategy.php

+6-11
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,28 @@
22

33
namespace Incenteev\HashedAssetBundle\Asset;
44

5+
use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
56
use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface;
67

78
class HashingVersionStrategy implements VersionStrategyInterface
89
{
9-
private $webRoot;
10+
private $hasher;
1011
private $format;
1112

12-
public function __construct(string $webRoot, string $format = null)
13+
public function __construct(AssetHasherInterface $hasher, string $format = null)
1314
{
1415
$this->format = $format ?: '%s?%s';
15-
$this->webRoot = $webRoot;
16+
$this->hasher = $hasher;
1617
}
1718

1819
public function getVersion($path)
1920
{
20-
$fullPath = $this->webRoot.'/'.ltrim($path, '/');
21-
22-
if (!is_file($fullPath)) {
23-
return '';
24-
}
25-
26-
return substr(sha1_file($fullPath), 0, 7);
21+
return $this->hasher->computeHash($path);
2722
}
2823

2924
public function applyVersion($path)
3025
{
31-
$versionized = sprintf($this->format, ltrim($path, '/'), $this->getVersion($path));
26+
$versionized = sprintf($this->format, ltrim($path, '/'), $this->hasher->computeHash($path));
3227

3328
if ($path && '/' === $path[0]) {
3429
return '/'.$versionized;

src/CacheWarmer/AssetFinder.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Incenteev\HashedAssetBundle\CacheWarmer;
4+
5+
use Symfony\Component\Finder\Finder;
6+
use Symfony\Component\Finder\SplFileInfo;
7+
8+
class AssetFinder
9+
{
10+
private $webRoot;
11+
12+
public function __construct(string $webRoot)
13+
{
14+
$this->webRoot = $webRoot;
15+
}
16+
17+
public function getAssetPaths(): \Traversable
18+
{
19+
$finder = (new Finder())->files()
20+
->in($this->webRoot);
21+
22+
/** @var SplFileInfo $file */
23+
foreach ($finder as $file) {
24+
yield $file->getRelativePathname();
25+
}
26+
}
27+
}

src/CacheWarmer/HashCacheWarmer.php

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Incenteev\HashedAssetBundle\CacheWarmer;
4+
5+
use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
6+
use Incenteev\HashedAssetBundle\Hashing\CachedHasher;
7+
use Psr\Cache\CacheItemPoolInterface;
8+
use Symfony\Component\Cache\Adapter\AdapterInterface;
9+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
10+
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
11+
use Symfony\Component\Cache\Adapter\ProxyAdapter;
12+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
13+
14+
final class HashCacheWarmer implements CacheWarmerInterface
15+
{
16+
private $assetFinder;
17+
private $cacheFile;
18+
private $hasher;
19+
private $fallbackPool;
20+
21+
public function __construct(AssetFinder $assetFinder, string $cacheFile, AssetHasherInterface $hasher, CacheItemPoolInterface $fallbackPool)
22+
{
23+
$this->assetFinder = $assetFinder;
24+
$this->cacheFile = $cacheFile;
25+
$this->hasher = $hasher;
26+
27+
if (!$fallbackPool instanceof AdapterInterface) {
28+
$fallbackPool = new ProxyAdapter($fallbackPool);
29+
}
30+
31+
$this->fallbackPool = $fallbackPool;
32+
}
33+
34+
public function warmUp($cacheDir)
35+
{
36+
$phpArrayPool = new PhpArrayAdapter($this->cacheFile, $this->fallbackPool);
37+
$arrayPool = new ArrayAdapter(0, false);
38+
39+
$hasher = new CachedHasher($this->hasher, $arrayPool);
40+
41+
foreach ($this->assetFinder->getAssetPaths() as $path) {
42+
$hasher->computeHash($path);
43+
}
44+
45+
$values = $arrayPool->getValues();
46+
$phpArrayPool->warmUp($values);
47+
48+
foreach ($values as $k => $v) {
49+
$item = $this->fallbackPool->getItem($k);
50+
$this->fallbackPool->saveDeferred($item->set($v));
51+
}
52+
$this->fallbackPool->commit();
53+
}
54+
55+
public function isOptional()
56+
{
57+
return true;
58+
}
59+
}

src/DependencyInjection/IncenteevHashedAssetExtension.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ protected function loadInternal(array $config, ContainerBuilder $container)
1717
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
1818
$loader->load('services.xml');
1919

20+
$container->getDefinition('incenteev_hashed_asset.file_hasher')
21+
->replaceArgument(0, $config['web_root']);
22+
2023
$container->getDefinition('incenteev_hashed_asset.strategy')
21-
->replaceArgument(0, $config['web_root'])
2224
->replaceArgument(1, $config['version_format']);
25+
26+
if (!$container->getParameter('kernel.debug')) {
27+
$loader->load('cache.xml');
28+
29+
$container->getDefinition('incenteev_hashed_asset.asset_finder')
30+
->replaceArgument(0, $config['web_root']);
31+
}
2332
}
2433
}

src/Hashing/AssetHasherInterface.php

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Incenteev\HashedAssetBundle\Hashing;
4+
5+
interface AssetHasherInterface
6+
{
7+
public function computeHash(string $path): string;
8+
}

src/Hashing/CachedHasher.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Incenteev\HashedAssetBundle\Hashing;
4+
5+
use Psr\Cache\CacheItemPoolInterface;
6+
7+
final class CachedHasher implements AssetHasherInterface
8+
{
9+
private $hasher;
10+
private $cache;
11+
12+
public function __construct(AssetHasherInterface $hasher, CacheItemPoolInterface $cache)
13+
{
14+
$this->hasher = $hasher;
15+
$this->cache = $cache;
16+
}
17+
18+
public function computeHash(string $path): string
19+
{
20+
// The hashing implementation does not care about leading slashes in the path, so share cache keys for them
21+
$item = $this->cache->getItem(base64_encode(ltrim($path, '/')));
22+
23+
if ($item->isHit()) {
24+
return $item->get();
25+
}
26+
27+
$hash = $this->hasher->computeHash($path);
28+
29+
$item->set($hash);
30+
$this->cache->save($item);
31+
32+
return $hash;
33+
}
34+
}

src/Hashing/FileHasher.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Incenteev\HashedAssetBundle\Hashing;
4+
5+
final class FileHasher implements AssetHasherInterface
6+
{
7+
private $webRoot;
8+
9+
public function __construct(string $webRoot)
10+
{
11+
$this->webRoot = $webRoot;
12+
}
13+
public function computeHash(string $path): string
14+
{
15+
$fullPath = $this->webRoot.'/'.ltrim($path, '/');
16+
17+
if (!is_file($fullPath)) {
18+
return '';
19+
}
20+
21+
return substr(sha1_file($fullPath), 0, 7);
22+
}
23+
}

src/Resources/config/cache.xml

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<parameters>
8+
<parameter key="incenteev_hashed_asset.cache.file">%kernel.cache_dir%/incenteev_asset_hashes.php</parameter>
9+
</parameters>
10+
11+
<services>
12+
<service id="incenteev_hashed_asset.cached_hasher" class="Incenteev\HashedAssetBundle\Hashing\CachedHasher" public="false" decorates="incenteev_hashed_asset.asset_hasher">
13+
<argument type="service" id="incenteev_hashed_asset.cached_hasher.inner" />
14+
<argument type="service">
15+
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
16+
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
17+
<argument>%incenteev_hashed_asset.cache.file%</argument>
18+
<argument type="service" id="cache.incenteev_hashed_asset" />
19+
</service>
20+
</argument>
21+
</service>
22+
23+
<service id="incenteev_hashed_asset.cache_warmer" class="Incenteev\HashedAssetBundle\CacheWarmer\HashCacheWarmer" public="false">
24+
<argument type="service" id="incenteev_hashed_asset.asset_finder" />
25+
<argument>%incenteev_hashed_asset.cache.file%</argument>
26+
<argument type="service" id="incenteev_hashed_asset.file_hasher" />
27+
<argument type="service" id="cache.incenteev_hashed_asset" />
28+
<tag name="kernel.cache_warmer" />
29+
</service>
30+
31+
<service id="incenteev_hashed_asset.asset_finder" class="Incenteev\HashedAssetBundle\CacheWarmer\AssetFinder" public="false">
32+
<argument />
33+
</service>
34+
35+
<service id="cache.incenteev_hashed_asset" parent="cache.system" public="false" />
36+
</services>
37+
</container>

src/Resources/config/services.xml

+6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
<services>
88
<service id="incenteev_hashed_asset.strategy" class="Incenteev\HashedAssetBundle\Asset\HashingVersionStrategy" public="false">
9+
<argument type="service" id="incenteev_hashed_asset.asset_hasher" />
910
<argument />
11+
</service>
12+
13+
<service id="incenteev_hashed_asset.asset_hasher" alias="incenteev_hashed_asset.file_hasher" public="false" />
14+
15+
<service id="incenteev_hashed_asset.file_hasher" class="Incenteev\HashedAssetBundle\Hashing\FileHasher" public="false">
1016
<argument />
1117
</service>
1218
</services>

tests/Asset/HashingVersionStrategyTest.php

+18-22
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,42 @@
33
namespace Incenteev\HashedAssetBundle\Tests\Asset;
44

55
use Incenteev\HashedAssetBundle\Asset\HashingVersionStrategy;
6+
use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
67
use PHPUnit\Framework\TestCase;
78

89
class HashingVersionStrategyTest extends TestCase
910
{
10-
/**
11-
* @dataProvider getAssetVersions
12-
*/
13-
public function testGetVersion($path, $version)
11+
public function testGetVersion()
1412
{
15-
$versionStrategy = new HashingVersionStrategy(__DIR__.'/fixtures');
13+
$hasher = $this->prophesize(AssetHasherInterface::class);
14+
$hasher->computeHash('test')->willReturn('foo');
1615

17-
$this->assertEquals($version, $versionStrategy->getVersion($path));
18-
}
16+
$versionStrategy = new HashingVersionStrategy($hasher->reveal());
1917

20-
public static function getAssetVersions()
21-
{
22-
yield ['asset1.txt', 'd0c0575'];
23-
yield ['asset2.txt', 'c1cf85a'];
24-
yield ['/asset2.txt', 'c1cf85a'];
25-
yield ['asset3.txt', ''];
18+
$this->assertEquals('foo', $versionStrategy->getVersion('test'));
2619
}
2720

2821
/**
2922
* @dataProvider getVersionedAssets
3023
*/
31-
public function testApplyVersion($path, $expected, $format = null)
24+
public function testApplyVersion($path, $expected, $hash, $format = null)
3225
{
33-
$versionStrategy = new HashingVersionStrategy(__DIR__.'/fixtures', $format);
26+
$hasher = $this->prophesize(AssetHasherInterface::class);
27+
$hasher->computeHash($path)->willReturn($hash);
28+
29+
$versionStrategy = new HashingVersionStrategy($hasher->reveal(), $format);
3430

3531
$this->assertEquals($expected, $versionStrategy->applyVersion($path));
3632
}
3733

3834
public static function getVersionedAssets()
3935
{
40-
yield ['asset1.txt', 'asset1.txt?d0c0575'];
41-
yield ['asset2.txt', 'asset2.txt?c1cf85a'];
42-
yield ['/asset2.txt', '/asset2.txt?c1cf85a'];
43-
yield ['asset3.txt', 'asset3.txt?'];
44-
yield ['asset2.txt', 'c1cf85a/asset2.txt', '%2$s/%1$s'];
45-
yield ['/asset2.txt', '/c1cf85a/asset2.txt', '%2$s/%1$s'];
46-
yield ['/asset2.txt', '/c1cf85a/asset2.txt', '%2$s/%s'];
36+
yield ['asset1.txt', 'asset1.txt?d0c0575', 'd0c0575'];
37+
yield ['asset2.txt', 'asset2.txt?c1cf85a', 'c1cf85a'];
38+
yield ['/asset2.txt', '/asset2.txt?c1cf85a', 'c1cf85a'];
39+
yield ['asset3.txt', 'asset3.txt?', ''];
40+
yield ['asset2.txt', 'c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%1$s'];
41+
yield ['/asset2.txt', '/c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%1$s'];
42+
yield ['/asset2.txt', '/c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%s'];
4743
}
4844
}

0 commit comments

Comments
 (0)