Skip to content

Commit

Permalink
Merge pull request #1 from Incenteev/cache
Browse files Browse the repository at this point in the history
Implement caching of the asset hashes for prod
  • Loading branch information
stof authored Jun 9, 2017
2 parents 192fbcf + cedf958 commit 3149cca
Show file tree
Hide file tree
Showing 21 changed files with 406 additions and 63 deletions.
5 changes: 1 addition & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ matrix:
fast_finish: true
include:
- php: 7.0
env: COMPOSER_FLAGS="--prefer-lowest"SYMFONY_DEPRECATIONS_HELPER=weak
env: COMPOSER_FLAGS="--prefer-lowest" SYMFONY_DEPRECATIONS_HELPER=weak
- php: 7.1
env: DEPENDENCIES=dev SYMFONY_DEPRECATIONS_HELPER=weak
- php: 7.0
env: SYMFONY_VERSION=2.8.*
allow_failures:
- php: nightly

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

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

install: composer update $COMPOSER_FLAGS
Expand Down
15 changes: 0 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,6 @@ framework:
version_strategy: incenteev_hashed_asset.strategy
```
If you are using Symfony <3.1, this configuration setting is not available.
Here is the workaround:
```yaml
# app/config/config.yml
framework:
assets:
version: dummy # set a dummy version so that the package does not use the empty version

services:
assets._version__default:
alias: incenteev_hashed_asset.strategy
# If you use additional packages, you may need to create additional aliases for other packages than `_default`
```

## Advanced configuration
The default configuration should fit common needs, but the bundle exposes
Expand Down
11 changes: 7 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
],
"require": {
"php": "^7.0",
"symfony/asset": "^2.8 || ^3.0",
"symfony/config": "^2.8 || ^3.0",
"symfony/dependency-injection": "^2.8 || ^3.0",
"symfony/http-kernel": "^2.8 || ^3.0"
"symfony/asset": "^3.2",
"symfony/cache": "^3.2",
"symfony/config": "^3.2",
"symfony/dependency-injection": "^3.2",
"symfony/finder": "^3.2",
"symfony/framework-bundle": "^3.2",
"symfony/http-kernel": "^3.2"
},
"require-dev": {
"symfony/phpunit-bridge": "^3.2"
Expand Down
17 changes: 6 additions & 11 deletions src/Asset/HashingVersionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,28 @@

namespace Incenteev\HashedAssetBundle\Asset;

use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface;

class HashingVersionStrategy implements VersionStrategyInterface
{
private $webRoot;
private $hasher;
private $format;

public function __construct(string $webRoot, string $format = null)
public function __construct(AssetHasherInterface $hasher, string $format = null)
{
$this->format = $format ?: '%s?%s';
$this->webRoot = $webRoot;
$this->hasher = $hasher;
}

public function getVersion($path)
{
$fullPath = $this->webRoot.'/'.ltrim($path, '/');

if (!is_file($fullPath)) {
return '';
}

return substr(sha1_file($fullPath), 0, 7);
return $this->hasher->computeHash($path);
}

public function applyVersion($path)
{
$versionized = sprintf($this->format, ltrim($path, '/'), $this->getVersion($path));
$versionized = sprintf($this->format, ltrim($path, '/'), $this->hasher->computeHash($path));

if ($path && '/' === $path[0]) {
return '/'.$versionized;
Expand Down
27 changes: 27 additions & 0 deletions src/CacheWarmer/AssetFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Incenteev\HashedAssetBundle\CacheWarmer;

use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

class AssetFinder
{
private $webRoot;

public function __construct(string $webRoot)
{
$this->webRoot = $webRoot;
}

public function getAssetPaths(): \Traversable
{
$finder = (new Finder())->files()
->in($this->webRoot);

/** @var SplFileInfo $file */
foreach ($finder as $file) {
yield $file->getRelativePathname();
}
}
}
59 changes: 59 additions & 0 deletions src/CacheWarmer/HashCacheWarmer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace Incenteev\HashedAssetBundle\CacheWarmer;

use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
use Incenteev\HashedAssetBundle\Hashing\CachedHasher;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

final class HashCacheWarmer implements CacheWarmerInterface
{
private $assetFinder;
private $cacheFile;
private $hasher;
private $fallbackPool;

public function __construct(AssetFinder $assetFinder, string $cacheFile, AssetHasherInterface $hasher, CacheItemPoolInterface $fallbackPool)
{
$this->assetFinder = $assetFinder;
$this->cacheFile = $cacheFile;
$this->hasher = $hasher;

if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}

$this->fallbackPool = $fallbackPool;
}

public function warmUp($cacheDir)
{
$phpArrayPool = new PhpArrayAdapter($this->cacheFile, $this->fallbackPool);
$arrayPool = new ArrayAdapter(0, false);

$hasher = new CachedHasher($this->hasher, $arrayPool);

foreach ($this->assetFinder->getAssetPaths() as $path) {
$hasher->computeHash($path);
}

$values = $arrayPool->getValues();
$phpArrayPool->warmUp($values);

foreach ($values as $k => $v) {
$item = $this->fallbackPool->getItem($k);
$this->fallbackPool->saveDeferred($item->set($v));
}
$this->fallbackPool->commit();
}

public function isOptional()
{
return true;
}
}
11 changes: 10 additions & 1 deletion src/DependencyInjection/IncenteevHashedAssetExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ protected function loadInternal(array $config, ContainerBuilder $container)
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');

$container->getDefinition('incenteev_hashed_asset.file_hasher')
->replaceArgument(0, $config['web_root']);

$container->getDefinition('incenteev_hashed_asset.strategy')
->replaceArgument(0, $config['web_root'])
->replaceArgument(1, $config['version_format']);

if (!$container->getParameter('kernel.debug')) {
$loader->load('cache.xml');

$container->getDefinition('incenteev_hashed_asset.asset_finder')
->replaceArgument(0, $config['web_root']);
}
}
}
8 changes: 8 additions & 0 deletions src/Hashing/AssetHasherInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Incenteev\HashedAssetBundle\Hashing;

interface AssetHasherInterface
{
public function computeHash(string $path): string;
}
34 changes: 34 additions & 0 deletions src/Hashing/CachedHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Incenteev\HashedAssetBundle\Hashing;

use Psr\Cache\CacheItemPoolInterface;

final class CachedHasher implements AssetHasherInterface
{
private $hasher;
private $cache;

public function __construct(AssetHasherInterface $hasher, CacheItemPoolInterface $cache)
{
$this->hasher = $hasher;
$this->cache = $cache;
}

public function computeHash(string $path): string
{
// The hashing implementation does not care about leading slashes in the path, so share cache keys for them
$item = $this->cache->getItem(base64_encode(ltrim($path, '/')));

if ($item->isHit()) {
return $item->get();
}

$hash = $this->hasher->computeHash($path);

$item->set($hash);
$this->cache->save($item);

return $hash;
}
}
23 changes: 23 additions & 0 deletions src/Hashing/FileHasher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Incenteev\HashedAssetBundle\Hashing;

final class FileHasher implements AssetHasherInterface
{
private $webRoot;

public function __construct(string $webRoot)
{
$this->webRoot = $webRoot;
}
public function computeHash(string $path): string
{
$fullPath = $this->webRoot.'/'.ltrim($path, '/');

if (!is_file($fullPath)) {
return '';
}

return substr(sha1_file($fullPath), 0, 7);
}
}
37 changes: 37 additions & 0 deletions src/Resources/config/cache.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<parameters>
<parameter key="incenteev_hashed_asset.cache.file">%kernel.cache_dir%/incenteev_asset_hashes.php</parameter>
</parameters>

<services>
<service id="incenteev_hashed_asset.cached_hasher" class="Incenteev\HashedAssetBundle\Hashing\CachedHasher" public="false" decorates="incenteev_hashed_asset.asset_hasher">
<argument type="service" id="incenteev_hashed_asset.cached_hasher.inner" />
<argument type="service">
<service class="Symfony\Component\Cache\Adapter\PhpArrayAdapter">
<factory class="Symfony\Component\Cache\Adapter\PhpArrayAdapter" method="create" />
<argument>%incenteev_hashed_asset.cache.file%</argument>
<argument type="service" id="cache.incenteev_hashed_asset" />
</service>
</argument>
</service>

<service id="incenteev_hashed_asset.cache_warmer" class="Incenteev\HashedAssetBundle\CacheWarmer\HashCacheWarmer" public="false">
<argument type="service" id="incenteev_hashed_asset.asset_finder" />
<argument>%incenteev_hashed_asset.cache.file%</argument>
<argument type="service" id="incenteev_hashed_asset.file_hasher" />
<argument type="service" id="cache.incenteev_hashed_asset" />
<tag name="kernel.cache_warmer" />
</service>

<service id="incenteev_hashed_asset.asset_finder" class="Incenteev\HashedAssetBundle\CacheWarmer\AssetFinder" public="false">
<argument />
</service>

<service id="cache.incenteev_hashed_asset" parent="cache.system" public="false" />
</services>
</container>
6 changes: 6 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

<services>
<service id="incenteev_hashed_asset.strategy" class="Incenteev\HashedAssetBundle\Asset\HashingVersionStrategy" public="false">
<argument type="service" id="incenteev_hashed_asset.asset_hasher" />
<argument />
</service>

<service id="incenteev_hashed_asset.asset_hasher" alias="incenteev_hashed_asset.file_hasher" public="false" />

<service id="incenteev_hashed_asset.file_hasher" class="Incenteev\HashedAssetBundle\Hashing\FileHasher" public="false">
<argument />
</service>
</services>
Expand Down
40 changes: 18 additions & 22 deletions tests/Asset/HashingVersionStrategyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,42 @@
namespace Incenteev\HashedAssetBundle\Tests\Asset;

use Incenteev\HashedAssetBundle\Asset\HashingVersionStrategy;
use Incenteev\HashedAssetBundle\Hashing\AssetHasherInterface;
use PHPUnit\Framework\TestCase;

class HashingVersionStrategyTest extends TestCase
{
/**
* @dataProvider getAssetVersions
*/
public function testGetVersion($path, $version)
public function testGetVersion()
{
$versionStrategy = new HashingVersionStrategy(__DIR__.'/fixtures');
$hasher = $this->prophesize(AssetHasherInterface::class);
$hasher->computeHash('test')->willReturn('foo');

$this->assertEquals($version, $versionStrategy->getVersion($path));
}
$versionStrategy = new HashingVersionStrategy($hasher->reveal());

public static function getAssetVersions()
{
yield ['asset1.txt', 'd0c0575'];
yield ['asset2.txt', 'c1cf85a'];
yield ['/asset2.txt', 'c1cf85a'];
yield ['asset3.txt', ''];
$this->assertEquals('foo', $versionStrategy->getVersion('test'));
}

/**
* @dataProvider getVersionedAssets
*/
public function testApplyVersion($path, $expected, $format = null)
public function testApplyVersion($path, $expected, $hash, $format = null)
{
$versionStrategy = new HashingVersionStrategy(__DIR__.'/fixtures', $format);
$hasher = $this->prophesize(AssetHasherInterface::class);
$hasher->computeHash($path)->willReturn($hash);

$versionStrategy = new HashingVersionStrategy($hasher->reveal(), $format);

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

public static function getVersionedAssets()
{
yield ['asset1.txt', 'asset1.txt?d0c0575'];
yield ['asset2.txt', 'asset2.txt?c1cf85a'];
yield ['/asset2.txt', '/asset2.txt?c1cf85a'];
yield ['asset3.txt', 'asset3.txt?'];
yield ['asset2.txt', 'c1cf85a/asset2.txt', '%2$s/%1$s'];
yield ['/asset2.txt', '/c1cf85a/asset2.txt', '%2$s/%1$s'];
yield ['/asset2.txt', '/c1cf85a/asset2.txt', '%2$s/%s'];
yield ['asset1.txt', 'asset1.txt?d0c0575', 'd0c0575'];
yield ['asset2.txt', 'asset2.txt?c1cf85a', 'c1cf85a'];
yield ['/asset2.txt', '/asset2.txt?c1cf85a', 'c1cf85a'];
yield ['asset3.txt', 'asset3.txt?', ''];
yield ['asset2.txt', 'c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%1$s'];
yield ['/asset2.txt', '/c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%1$s'];
yield ['/asset2.txt', '/c1cf85a/asset2.txt', 'c1cf85a', '%2$s/%s'];
}
}
Loading

0 comments on commit 3149cca

Please sign in to comment.