diff --git a/Imagine/Cache/Resolver/ProxyResolver.php b/Imagine/Cache/Resolver/ProxyResolver.php new file mode 100644 index 000000000..1e7ba979d --- /dev/null +++ b/Imagine/Cache/Resolver/ProxyResolver.php @@ -0,0 +1,118 @@ + + */ +class ProxyResolver implements ResolverInterface +{ + /** + * @var ResolverInterface + */ + private $resolver; + + /** + * a list of proxy hosts (picks a random one for each generation to seed browser requests among multiple hosts) + * + * @var array + */ + private $hosts = array(); + + /** + * @param ResolverInterface $resolver + */ + public function __construct(ResolverInterface $resolver, array $hosts) + { + $this->resolver = $resolver; + $this->hosts = $hosts; + } + + /** + * {@inheritDoc} + */ + public function resolve(Request $request, $path, $filter) + { + $response = $this->resolver->resolve($request, $path, $filter); + $this->rewriteResponse($response); + + return $response; + } + + /** + * {@inheritDoc} + */ + public function store(Response $response, $targetPath, $filter) + { + $response = $this->resolver->store($response, $targetPath, $filter); + $this->rewriteResponse($response); + + return $response; + } + + /** + * {@inheritDoc} + */ + public function getBrowserPath($path, $filter, $absolute = false) + { + $response = $this->resolver->getBrowserPath($path, $filter, $absolute); + $this->rewriteResponse($response); + + return $response; + } + + /** + * {@inheritDoc} + */ + public function remove($targetPath, $filter) + { + return $this->resolver->remove($targetPath, $filter); + } + + /** + * {@inheritDoc} + */ + public function clear($cachePrefix) + { + $this->resolver->clear($cachePrefix); + } + + private function rewriteResponse($response) + { + if (!$response instanceof RedirectResponse || !$this->hosts) { + return; + } + + if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) { + $redirectLocation = $response->headers->get('Location'); + } else { + $redirectLocation = $response->getTargetUrl(); + } + $path = parse_url($redirectLocation, PHP_URL_PATH); + + if ($path == $redirectLocation) { + //relative path, so strip of SCRIPT_FILE_NAME if existient + $path = substr($path, (strpos($path, '.php') !== false ? strpos($path, '.php') + 4 : 0)); + } + + if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) { + $response->headers->set('Location', $this->createProxyUrl($path)); + } else { + $response->setTargetUrl($this->createProxyUrl($path)); + } + } + + private function createProxyUrl($path) + { + $domain = $this->hosts[rand(0, count($this->hosts) - 1)]; + + return $domain . $path; + } +} diff --git a/Resources/doc/cache-resolver/proxy.md b/Resources/doc/cache-resolver/proxy.md new file mode 100644 index 000000000..8488399cb --- /dev/null +++ b/Resources/doc/cache-resolver/proxy.md @@ -0,0 +1,26 @@ +# ProxyResolver + +The ProxyResolver is a `decorator` for every other Resolver + +This Resolver adds the possibility to use Proxy Hosts for your Assets. +If no Proxy Domains are set, it behaves like the underlying `Resolver` + +## Set Proxy Domains + +In order to use this Resolver you must create a Service and inject some domains and your underlying Resolver + +``` yaml +services: + acme.imagine.cache.resolver.proxy: + class: Liip\ImagineBundle\Imagine\Cache\Resolver\ProxyResolver + arguments: + - "@acme.imagine.cache.resolver.amazon_s3" + - [ 'http://images0.domain.com', 'http://images1.domain.com','http://images2.domain.com' ] + tags: + - { name: 'liip_imagine.cache.resolver', resolver: 'proxy' } +``` + +Now your Resolver would generate following Urls `http://images0.domain.com/thumbs/article_thumb/foo.jpg` instead of the original path from the underlying Resolver `bucket.s3.awsamazoncloud.com/thumbs/article_thumb/foo.jpg` for every relevant Action. + +- [Back to cache resolvers](../cache-resolvers.md) +- [Back to the index](../index.md) diff --git a/Resources/doc/cache-resolvers.md b/Resources/doc/cache-resolvers.md index d3c2ceff1..4d82fbcb1 100644 --- a/Resources/doc/cache-resolvers.md +++ b/Resources/doc/cache-resolvers.md @@ -3,6 +3,7 @@ * [AmazonS3](cache-resolver/amazons3.md) * [AwsS3](cache-resolver/aws_s3.md) - for SDK version 2 * [CacheResolver](cache-resolver/cache.md) +* [ProxyResolver](cache-resolver/proxy.md) # Custom cache resolver diff --git a/Tests/Imagine/Cache/Resolver/ProxyResolverTest.php b/Tests/Imagine/Cache/Resolver/ProxyResolverTest.php new file mode 100644 index 000000000..6077a78ac --- /dev/null +++ b/Tests/Imagine/Cache/Resolver/ProxyResolverTest.php @@ -0,0 +1,147 @@ + + */ +class ProxyResolverTest extends AbstractTest +{ + /** + * @var ResolverInterface + */ + private $primaryResolver; + + /** + * @var ProxyResolver + */ + private $resolver; + + public function setUp() + { + $this->primaryResolver = $this->getMock('Liip\ImagineBundle\Imagine\Cache\Resolver\ResolverInterface'); + + $this->resolver = new ProxyResolver($this->primaryResolver, array('http://images.example.com')); + } + + public function testResolveWithResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('resolve') + ->will($this->returnValue(new RedirectResponse('app_dev.php/thumbs/foo/bar/bazz.png'))); + + $result = $this->resolver->resolve(new Request(), '/foo/bar/bazz.png', 'test'); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $result); + + if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->headers->get('Location')); + } else { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->getTargetUrl()); + } + } + + public function testResolveWithoutResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('resolve') + ->will($this->returnValue('app_dev.php/thumbs/foo/bar/bazz.png')); + + $result = $this->resolver->resolve(new Request(), '/foo/bar/bazz.png', 'test'); + + $this->assertEquals('app_dev.php/thumbs/foo/bar/bazz.png', $result); + } + + public function testStoreWithResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('store') + ->will($this->returnValue(new RedirectResponse('http://foo.com/thumbs/foo/bar/bazz.png'))); + + $result = $this->resolver->store(new Response(), '/foo/bar/bazz.png', 'test'); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $result); + + if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->headers->get('Location')); + } else { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->getTargetUrl()); + } + } + + public function testStoreWithoutResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('store') + ->will($this->returnValue('http://foo.com/thumbs/foo/bar/bazz.png')); + + $result = $this->resolver->store(new Response(), '/foo/bar/bazz.png', 'test'); + + $this->assertEquals('http://foo.com/thumbs/foo/bar/bazz.png', $result); + } + + public function testGetBrowserPathWithResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('getBrowserPath') + ->will($this->returnValue(new RedirectResponse('s3://myfunkybucket/thumbs/foo/bar/bazz.png'))); + + $result = $this->resolver->getBrowserPath('/foo/bar/bazz.png', 'test'); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $result); + + if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->headers->get('Location')); + } else { + $this->assertEquals('http://images.example.com/thumbs/foo/bar/bazz.png', $result->getTargetUrl()); + } + } + + public function testGetBrowserPathWithoutResponse() + { + $this->primaryResolver + ->expects($this->once()) + ->method('getBrowserPath') + ->will($this->returnValue('s3://myfunkybucket/thumbs/foo/bar/bazz.png')); + + $result = $this->resolver->getBrowserPath('/foo/bar/bazz.png', 'test'); + + $this->assertEquals('s3://myfunkybucket/thumbs/foo/bar/bazz.png', $result); + } + + public function testRemove() + { + $this->primaryResolver + ->expects($this->once()) + ->method('remove'); + + $this->resolver->remove('/foo/bar/bazz.png', 'test'); + } + + public function testClear() + { + $this->primaryResolver + ->expects($this->once()) + ->method('clear'); + + $this->resolver->clear('test'); + } + +} \ No newline at end of file