diff --git a/.gitignore b/.gitignore
index 319b3826f..a4ada9c75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
/phpunit.xml
+composer.lock
+composer.phar
+vendor/
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..686495d2a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+ - 5.5
+
+env:
+ - SYMFONY_VERSION=2.0.*
+ - SYMFONY_VERSION=2.1.*
+ - SYMFONY_VERSION=2.2.*
+ - SYMFONY_VERSION=2.3.*
+ - SYMFONY_VERSION=dev-master
+
+before_script:
+ - composer require symfony/framework-bundle:${SYMFONY_VERSION} --prefer-source
+ - composer install --dev --prefer-source
+
+script: phpunit --coverage-text
+
+notifications:
+ email:
+ - travis-ci@liip.ch
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..f99573792
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+Changelog
+=========
+
+* **2013-09-17**: [PHPCR loader] DoctrinePHPCRLoader is now deprecated, as the
+ CmfMediaBundle provides a more reliable loader that is already provided as a
+ service. See http://symfony.com/doc/master/cmf/bundles/media.html#liipimagine
\ No newline at end of file
diff --git a/Controller/ImagineController.php b/Controller/ImagineController.php
index e11936a87..c2979924d 100644
--- a/Controller/ImagineController.php
+++ b/Controller/ImagineController.php
@@ -2,49 +2,48 @@
namespace Liip\ImagineBundle\Controller;
+use Liip\ImagineBundle\Imagine\Cache\CacheManager;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
+use Liip\ImagineBundle\Imagine\Filter\FilterManager;
+
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Liip\ImagineBundle\Imagine\CachePathResolver;
-use Liip\ImagineBundle\Imagine\DataLoader\LoaderInterface;
-use Liip\ImagineBundle\Imagine\Filter\FilterManager;
class ImagineController
{
/**
- * @var LoaderInterface
+ * @var DataManager
*/
- private $dataLoader;
+ protected $dataManager;
/**
* @var FilterManager
*/
- private $filterManager;
+ protected $filterManager;
/**
- * @var CachePathResolver
+ * @var CacheManager
*/
- private $cachePathResolver;
+ protected $cacheManager;
/**
- * Constructor
+ * Constructor.
*
- * @param LoaderInterface $dataLoader
+ * @param DataManager $dataManager
* @param FilterManager $filterManager
- * @param CachePathResolver $cachePathResolver
+ * @param CacheManager $cacheManager
*/
- public function __construct(LoaderInterface $dataLoader, FilterManager $filterManager, CachePathResolver $cachePathResolver = null)
+ public function __construct(DataManager $dataManager, FilterManager $filterManager, CacheManager $cacheManager)
{
- $this->dataLoader = $dataLoader;
+ $this->dataManager = $dataManager;
$this->filterManager = $filterManager;
- $this->cachePathResolver = $cachePathResolver;
+ $this->cacheManager = $cacheManager;
}
/**
- * This action applies a given filter to a given image,
- * optionally saves the image and
- * outputs it to the browser at the same time
+ * This action applies a given filter to a given image, optionally saves the image and outputs it to the browser at the same time.
*
- * @param Symfony\Component\HttpFoundation\Request $request
+ * @param Request $request
* @param string $path
* @param string $filter
*
@@ -52,19 +51,16 @@ public function __construct(LoaderInterface $dataLoader, FilterManager $filterMa
*/
public function filterAction(Request $request, $path, $filter)
{
- $targetPath = false;
- if ($this->cachePathResolver) {
- $targetPath = $this->cachePathResolver->resolve($request, $path, $filter);
- if ($targetPath instanceof Response) {
- return $targetPath;
- }
+ $targetPath = $this->cacheManager->resolve($request, $path, $filter);
+ if ($targetPath instanceof Response) {
+ return $targetPath;
}
- $image = $this->dataLoader->find($path);
+ $image = $this->dataManager->find($filter, $path);
$response = $this->filterManager->get($request, $filter, $image, $path);
- if ($targetPath && $response->isSuccessful()) {
- $response = $this->cachePathResolver->store($response, $targetPath);
+ if ($targetPath) {
+ $response = $this->cacheManager->store($response, $targetPath, $filter);
}
return $response;
diff --git a/DependencyInjection/Compiler/CreateCacheDirectoriesCompilerPass.php b/DependencyInjection/Compiler/CreateCacheDirectoriesCompilerPass.php
index dd5e7c14d..a7c81f3ea 100644
--- a/DependencyInjection/Compiler/CreateCacheDirectoriesCompilerPass.php
+++ b/DependencyInjection/Compiler/CreateCacheDirectoriesCompilerPass.php
@@ -16,13 +16,14 @@ public function process(ContainerBuilder $container)
$webRoot = $container->getParameter('liip_imagine.web_root');
$cachePrefix = $container->getParameter('liip_imagine.cache_prefix');
$filters = $container->getParameter('liip_imagine.filters');
+ $mode = $container->getParameter('liip_imagine.cache_mkdir_mode');
foreach ($filters as $filter => $options) {
$dir = isset($options['path'])
? $webRoot.$options['path']
: $webRoot.$cachePrefix.'/'.$filter;
- if (!is_dir($dir) && !mkdir($dir, 0777, true)) {
+ if (!is_dir($dir) && !mkdir($dir, $mode, true)) {
throw new \RuntimeException(sprintf(
'Could not create directory for caching processed '.
'images in "%s"', $dir
diff --git a/DependencyInjection/Compiler/LoadersCompilerPass.php b/DependencyInjection/Compiler/LoadersCompilerPass.php
index 13fabd69c..c96ab24e2 100644
--- a/DependencyInjection/Compiler/LoadersCompilerPass.php
+++ b/DependencyInjection/Compiler/LoadersCompilerPass.php
@@ -16,7 +16,27 @@ public function process(ContainerBuilder $container)
$manager = $container->getDefinition('liip_imagine.filter.manager');
foreach ($tags as $id => $tag) {
- $manager->addMethodCall('addLoader', array($tag[0]['filter'], new Reference($id)));
+ $manager->addMethodCall('addLoader', array($tag[0]['loader'], new Reference($id)));
+ }
+ }
+
+ $tags = $container->findTaggedServiceIds('liip_imagine.data.loader');
+
+ if (count($tags) > 0 && $container->hasDefinition('liip_imagine.data.manager')) {
+ $manager = $container->getDefinition('liip_imagine.data.manager');
+
+ foreach ($tags as $id => $tag) {
+ $manager->addMethodCall('addLoader', array($tag[0]['loader'], new Reference($id)));
+ }
+ }
+
+ $tags = $container->findTaggedServiceIds('liip_imagine.cache.resolver');
+
+ if (count($tags) > 0 && $container->hasDefinition('liip_imagine.cache.manager')) {
+ $manager = $container->getDefinition('liip_imagine.cache.manager');
+
+ foreach ($tags as $id => $tag) {
+ $manager->addMethodCall('addResolver', array($tag[0]['resolver'], new Reference($id)));
}
}
}
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index 4bae850a6..3b1b4e832 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -2,8 +2,8 @@
namespace Liip\ImagineBundle\DependencyInjection;
-use Symfony\Component\Config\Definition\Builder\TreeBuilder,
- Symfony\Component\Config\Definition\ConfigurationInterface;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
@@ -28,9 +28,14 @@ public function getConfigTreeBuilder()
->end()
->end()
->scalarNode('web_root')->defaultValue('%kernel.root_dir%/../web')->end()
+ ->scalarNode('data_root')->defaultValue('%liip_imagine.web_root%')->end()
+ ->scalarNode('cache_mkdir_mode')->defaultValue(0777)->end()
->scalarNode('cache_prefix')->defaultValue('/media/cache')->end()
- ->scalarNode('cache')->defaultTrue()->end()
- ->scalarNode('loader')->defaultNull()->end()
+ ->scalarNode('cache')->defaultValue('web_path')->end()
+ ->scalarNode('cache_base_path')->defaultValue('')->end()
+ ->booleanNode('cache_clearer')->defaultValue(true)->end()
+ ->scalarNode('data_loader')->defaultValue('filesystem')->end()
+ ->scalarNode('controller_action')->defaultValue('liip_imagine.controller:filterAction')->end()
->arrayNode('formats')
->defaultValue(array())
->prototype('scalar')->end()
@@ -39,11 +44,21 @@ public function getConfigTreeBuilder()
->useAttributeAsKey('name')
->prototype('array')
->fixXmlConfig('filter', 'filters')
- ->useAttributeAsKey('name')
->children()
->scalarNode('path')->end()
->scalarNode('quality')->defaultValue(100)->end()
->scalarNode('format')->defaultNull()->end()
+ ->scalarNode('cache')->defaultNull()->end()
+ ->scalarNode('data_loader')->defaultNull()->end()
+ ->scalarNode('controller_action')->defaultNull()->end()
+ ->arrayNode('route')
+ ->defaultValue(array())
+ ->useAttributeAsKey('name')
+ ->prototype('array')
+ ->useAttributeAsKey('name')
+ ->prototype('variable')->end()
+ ->end()
+ ->end()
->arrayNode('filters')
->useAttributeAsKey('name')
->prototype('array')
diff --git a/DependencyInjection/LiipImagineExtension.php b/DependencyInjection/LiipImagineExtension.php
index 5420e47d0..d642de017 100644
--- a/DependencyInjection/LiipImagineExtension.php
+++ b/DependencyInjection/LiipImagineExtension.php
@@ -3,11 +3,13 @@
namespace Liip\ImagineBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
+
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
+
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
-use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpKernel\Kernel;
class LiipImagineExtension extends Extension
{
@@ -21,13 +23,20 @@ public function load(array $configs, ContainerBuilder $container)
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('imagine.xml');
+ if ($config['cache_clearer']) {
+ $loader->load('cache_clearer.xml');
+ }
+
$container->setAlias('liip_imagine', new Alias('liip_imagine.'.$config['driver']));
$cachePrefix = $config['cache_prefix'] ? '/'.trim($config['cache_prefix'], '/') : '';
$container->setParameter('liip_imagine.cache_prefix', $cachePrefix);
$container->setParameter('liip_imagine.web_root', $config['web_root']);
+ $container->setParameter('liip_imagine.data_root', $config['data_root']);
+ $container->setParameter('liip_imagine.cache_mkdir_mode', $config['cache_mkdir_mode']);
$container->setParameter('liip_imagine.formats', $config['formats']);
- $container->setParameter('liip_imagine.cache', $config['cache']);
+ $container->setParameter('liip_imagine.cache.resolver.default', $config['cache']);
+
foreach ($config['filter_sets'] as $filter => $options) {
if (isset($options['path'])) {
$config['filter_sets'][$filter]['path'] = '/'.trim($options['path'], '/');
@@ -35,14 +44,18 @@ public function load(array $configs, ContainerBuilder $container)
}
$container->setParameter('liip_imagine.filter_sets', $config['filter_sets']);
- if ($container->getParameter('liip_imagine.cache')) {
- $controller = $container->getDefinition('liip_imagine.controller');
- $controller->addArgument(new Reference('liip_imagine.cache.path.resolver'));
- }
+ $container->setParameter('liip_imagine.data.loader.default', $config['data_loader']);
+
+ $container->setParameter('liip_imagine.controller_action', $config['controller_action']);
- if (!empty($config['loader'])) {
- $controller = $container->getDefinition('liip_imagine.controller');
- $controller->replaceArgument(0, new Reference($config['loader']));
+ if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) {
+ $container->removeDefinition('liip_imagine.cache.clearer');
}
+
+ $container->setParameter('liip_imagine.cache.resolver.base_path', $config['cache_base_path']);
+
+ $resources = $container->hasParameter('twig.form.resources') ? $container->getParameter('twig.form.resources') : array();
+ $resources[] = 'LiipImagineBundle:Form:form_div_layout.html.twig';
+ $container->setParameter('twig.form.resources', $resources);
}
}
diff --git a/Form/Type/ImageType.php b/Form/Type/ImageType.php
new file mode 100644
index 000000000..7254a1bde
--- /dev/null
+++ b/Form/Type/ImageType.php
@@ -0,0 +1,52 @@
+
+ */
+class ImageType extends AbstractType
+{
+ public function buildView(FormView $view, FormInterface $form, array $options)
+ {
+ $view->vars['image_path'] = $options['image_path'];
+ $view->vars['image_filter'] = $options['image_filter'];
+ $view->vars['image_attr'] = $options['image_attr'];
+ $view->vars['link_url'] = $options['link_url'];
+ $view->vars['link_filter'] = $options['link_filter'];
+ $view->vars['link_attr'] = $options['link_attr'];
+ }
+
+ public function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setRequired(array(
+ 'image_path',
+ 'image_filter',
+ ));
+
+ $resolver->setDefaults(array(
+ 'image_attr' => array(),
+ 'link_url' => null,
+ 'link_filter' => null,
+ 'link_attr' => array(),
+ ));
+ }
+
+ public function getParent()
+ {
+ return 'file';
+ }
+
+ public function getName()
+ {
+ return 'liip_imagine_image';
+ }
+}
diff --git a/Imagine/Cache/CacheClearer.php b/Imagine/Cache/CacheClearer.php
new file mode 100644
index 000000000..1ec50e29b
--- /dev/null
+++ b/Imagine/Cache/CacheClearer.php
@@ -0,0 +1,45 @@
+
+ */
+class CacheClearer implements CacheClearerInterface
+{
+ /**
+ * @var CacheManager
+ */
+ protected $cacheManager;
+
+ /**
+ * @var string
+ */
+ protected $cachePrefix;
+
+ /**
+ * Constructor.
+ *
+ * @param CacheManager $cacheManager
+ * @param string $cachePrefix The prefix applied to all cached images.
+ */
+ public function __construct(CacheManager $cacheManager, $cachePrefix)
+ {
+ $this->cacheManager = $cacheManager;
+ $this->cachePrefix = $cachePrefix;
+ }
+
+ /**
+ * (non-PHPdoc)
+ * @see Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface::clear()
+ */
+ public function clear($cacheDir)
+ {
+ // $cacheDir contains the application cache, which we don't care about
+ $this->cacheManager->clearResolversCache($this->cachePrefix);
+ }
+}
diff --git a/Imagine/Cache/CacheManager.php b/Imagine/Cache/CacheManager.php
new file mode 100644
index 000000000..76b44bca0
--- /dev/null
+++ b/Imagine/Cache/CacheManager.php
@@ -0,0 +1,241 @@
+filterConfig = $filterConfig;
+ $this->router = $router;
+ $this->webRoot = realpath($webRoot);
+ $this->defaultResolver = $defaultResolver;
+ }
+
+ /**
+ * Adds a resolver to handle cached images for the given filter.
+ *
+ * @param string $filter
+ * @param ResolverInterface $resolver
+ *
+ * @return void
+ */
+ public function addResolver($filter, ResolverInterface $resolver)
+ {
+ $this->resolvers[$filter] = $resolver;
+
+ if ($resolver instanceof CacheManagerAwareInterface) {
+ $resolver->setCacheManager($this);
+ }
+ }
+
+ /**
+ * Returns the configured web root path.
+ *
+ * @return string
+ */
+ public function getWebRoot()
+ {
+ return $this->webRoot;
+ }
+
+ /**
+ * Gets a resolver for the given filter.
+ *
+ * In case there is no specific resolver, but a default resolver has been configured, the default will be returned.
+ *
+ * @param string $filter
+ *
+ * @return ResolverInterface
+ *
+ * @throws \InvalidArgumentException If neither a specific nor a default resolver is available.
+ */
+ protected function getResolver($filter)
+ {
+ $config = $this->filterConfig->get($filter);
+
+ $resolverName = empty($config['cache'])
+ ? $this->defaultResolver : $config['cache'];
+
+ if (!isset($this->resolvers[$resolverName])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Could not find resolver for "%s" filter type', $filter
+ ));
+ }
+
+ return $this->resolvers[$resolverName];
+ }
+
+ /**
+ * Gets filtered path for rendering in the browser.
+ *
+ * @see ResolverInterface::getBrowserPath
+ *
+ * @param string $path The path where the resolved file is expected.
+ * @param string $filter
+ * @param boolean $absolute
+ *
+ * @return string
+ */
+ public function getBrowserPath($path, $filter, $absolute = false)
+ {
+ return $this->getResolver($filter)->getBrowserPath($path, $filter, $absolute);
+ }
+
+ /**
+ * Returns a web accessible URL.
+ *
+ * @param string $path The path where the resolved file is expected.
+ * @param string $filter The name of the imagine filter in effect.
+ * @param bool $absolute Whether to generate an absolute URL or a relative path is accepted.
+ * In case the resolver does not support relative paths, it may ignore this flag.
+ *
+ * @return string
+ */
+ public function generateUrl($path, $filter, $absolute = false)
+ {
+ $config = $this->filterConfig->get($filter);
+
+ if (isset($config['format'])) {
+ $pathinfo = pathinfo($path);
+
+ // the extension should be forced and a directory is detected
+ if ((!isset($pathinfo['extension']) || $pathinfo['extension'] !== $config['format'])
+ && isset($pathinfo['dirname'])) {
+
+ if ('\\' === $pathinfo['dirname']) {
+ $pathinfo['dirname'] = '';
+ }
+
+ $path = $pathinfo['dirname'].'/'.$pathinfo['filename'].'.'.$config['format'];
+ }
+ }
+
+ $params = array('path' => ltrim($path, '/'));
+
+ return str_replace(
+ urlencode($params['path']),
+ urldecode($params['path']),
+ $this->router->generate('_imagine_'.$filter, $params, $absolute)
+ );
+ }
+
+ /**
+ * Resolves filtered path for rendering in the browser.
+ *
+ * @param Request $request
+ * @param string $path
+ * @param string $filter
+ *
+ * @return string|boolean|Response target path or false if filter has no
+ * resolver or a Response object from the resolver
+ *
+ * @throws NotFoundHttpException if the path can not be resolved
+ */
+ public function resolve(Request $request, $path, $filter)
+ {
+ if (false !== strpos($path, '/../') || 0 === strpos($path, '../')) {
+ throw new NotFoundHttpException(sprintf("Source image was searched with '%s' outside of the defined root path", $path));
+ }
+
+ try {
+ $resolver = $this->getResolver($filter);
+ } catch (\InvalidArgumentException $e) {
+ return false;
+ }
+
+ return $resolver->resolve($request, $path, $filter);
+ }
+
+ /**
+ * Store successful responses with the cache resolver.
+ *
+ * @see ResolverInterface::store
+ *
+ * @param Response $response
+ * @param string $targetPath
+ * @param string $filter
+ *
+ * @return Response
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ if ($response->isSuccessful()) {
+ $response = $this->getResolver($filter)->store($response, $targetPath, $filter);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Remove a cached image from the storage.
+ *
+ * @see ResolverInterface::remove
+ *
+ * @param string $targetPath
+ * @param string $filter
+ *
+ * @return bool
+ */
+ public function remove($targetPath, $filter)
+ {
+ return $this->getResolver($filter)->remove($targetPath, $filter);
+ }
+
+ /**
+ * Clear the cache of all resolvers.
+ *
+ * @see ResolverInterface::clear
+ *
+ * @param string $cachePrefix
+ *
+ * @return void
+ */
+ public function clearResolversCache($cachePrefix)
+ {
+ foreach ($this->resolvers as $resolver) {
+ $resolver->clear($cachePrefix);
+ }
+ }
+}
diff --git a/Imagine/Cache/CacheManagerAwareInterface.php b/Imagine/Cache/CacheManagerAwareInterface.php
new file mode 100644
index 000000000..e8baf53f0
--- /dev/null
+++ b/Imagine/Cache/CacheManagerAwareInterface.php
@@ -0,0 +1,11 @@
+cacheManager = $cacheManager;
+ }
+}
diff --git a/Imagine/Cache/Resolver/AbstractFilesystemResolver.php b/Imagine/Cache/Resolver/AbstractFilesystemResolver.php
new file mode 100644
index 000000000..b7c2c1021
--- /dev/null
+++ b/Imagine/Cache/Resolver/AbstractFilesystemResolver.php
@@ -0,0 +1,140 @@
+filesystem = $filesystem;
+ }
+
+ /**
+ * @param CacheManager $cacheManager
+ */
+ public function setCacheManager(CacheManager $cacheManager)
+ {
+ $this->cacheManager = $cacheManager;
+ }
+
+ /**
+ * Set the base path to
+ *
+ * @param $basePath
+ */
+ public function setBasePath($basePath)
+ {
+ $this->basePath = $basePath;
+ }
+
+ /**
+ * @param int $mkdirMode
+ */
+ public function setFolderPermissions ($folderPermissions)
+ {
+ $this->folderPermissions = $folderPermissions;
+ }
+
+ /**
+ * Stores the content into a static file.
+ *
+ * @param Response $response
+ * @param string $targetPath
+ * @param string $filter
+ *
+ * @return Response
+ *
+ * @throws \RuntimeException
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ $dir = pathinfo($targetPath, PATHINFO_DIRNAME);
+
+ $this->makeFolder($dir);
+
+ file_put_contents($targetPath, $response->getContent());
+
+ $response->setStatusCode(201);
+
+ return $response;
+ }
+
+ /**
+ * Removes a stored image resource.
+ *
+ * @param string $targetPath The target path provided by the resolve method.
+ * @param string $filter The name of the imagine filter in effect.
+ *
+ * @return bool Whether the file has been removed successfully.
+ */
+ public function remove($targetPath, $filter)
+ {
+ $filename = $this->getFilePath($targetPath, $filter);
+ $this->filesystem->remove($filename);
+
+ return !file_exists($filename);
+ }
+
+ /**
+ * @param string $dir
+ * @throws \RuntimeException
+ */
+ protected function makeFolder ($dir)
+ {
+ if (!is_dir($dir)) {
+ $parent = dirname($dir);
+ try {
+ $this->makeFolder($parent);
+ $this->filesystem->mkdir($dir);
+ $this->filesystem->chmod($dir, $this->folderPermissions);
+ } catch (IOException $e) {
+ throw new \RuntimeException(sprintf('Could not create directory %s', $dir), 0, $e);
+ }
+ }
+ }
+
+ /**
+ * Return the local filepath.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ *
+ * @param string $path The resource path to convert.
+ * @param string $filter The name of the imagine filter.
+ *
+ * @return string
+ */
+ abstract protected function getFilePath($path, $filter);
+}
diff --git a/Imagine/Cache/Resolver/AmazonS3Resolver.php b/Imagine/Cache/Resolver/AmazonS3Resolver.php
new file mode 100644
index 000000000..5a622bddc
--- /dev/null
+++ b/Imagine/Cache/Resolver/AmazonS3Resolver.php
@@ -0,0 +1,214 @@
+storage = $storage;
+
+ $this->bucket = $bucket;
+ $this->acl = $acl;
+
+ $this->objUrlOptions = $objUrlOptions;
+ }
+
+ /**
+ * Sets the logger to be used.
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param CacheManager $cacheManager
+ */
+ public function setCacheManager(CacheManager $cacheManager)
+ {
+ $this->cacheManager = $cacheManager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resolve(Request $request, $path, $filter)
+ {
+ $objectPath = $this->getObjectPath($path, $filter);
+ if ($this->objectExists($objectPath)) {
+ return new RedirectResponse($this->getObjectUrl($objectPath), 301);
+ }
+
+ return $objectPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ $storageResponse = $this->storage->create_object($this->bucket, $targetPath, array(
+ 'body' => $response->getContent(),
+ 'contentType' => $response->headers->get('Content-Type'),
+ 'length' => strlen($response->getContent()),
+ 'acl' => $this->acl,
+ ));
+
+ if ($storageResponse->isOK()) {
+ $response->setStatusCode(301);
+ $response->headers->set('Location', $this->getObjectUrl($targetPath));
+ } else {
+ if ($this->logger) {
+ $this->logger->warn('The object could not be created on Amazon S3.', array(
+ 'targetPath' => $targetPath,
+ 'filter' => $filter,
+ 's3_response' => $storageResponse,
+ ));
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getBrowserPath($path, $filter, $absolute = false)
+ {
+ $objectPath = $this->getObjectPath($path, $filter);
+ if ($this->objectExists($objectPath)) {
+ return $this->getObjectUrl($objectPath);
+ }
+
+ return $this->cacheManager->generateUrl($path, $filter, $absolute);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove($targetPath, $filter)
+ {
+ if (!$this->objectExists($targetPath)) {
+ // A non-existing object to delete: done!
+ return true;
+ }
+
+ return $this->storage->delete_object($this->bucket, $targetPath)->isOK();
+ }
+
+ /**
+ * Sets a single option to be passed when retrieving an objects URL.
+ *
+ * If the option is already set, it will be overwritten.
+ *
+ * @see \AmazonS3::get_object_url() for available options.
+ *
+ * @param string $key The name of the option.
+ * @param mixed $value The value to be set.
+ *
+ * @return AmazonS3Resolver $this
+ */
+ public function setObjectUrlOption($key, $value)
+ {
+ $this->objUrlOptions[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function clear($cachePrefix)
+ {
+ // TODO: implement cache clearing for Amazon S3 service
+ }
+
+ /**
+ * Returns the object path within the bucket.
+ *
+ * @param string $path The base path of the resource.
+ * @param string $filter The name of the imagine filter in effect.
+ *
+ * @return string The path of the object on S3.
+ */
+ protected function getObjectPath($path, $filter)
+ {
+ return str_replace('//', '/', $filter.'/'.$path);
+ }
+
+ /**
+ * Returns the URL for an object saved on Amazon S3.
+ *
+ * @param string $targetPath
+ *
+ * @return string
+ */
+ protected function getObjectUrl($targetPath)
+ {
+ return $this->storage->get_object_url($this->bucket, $targetPath, 0, $this->objUrlOptions);
+ }
+
+ /**
+ * Checks whether an object exists.
+ *
+ * @param string $objectPath
+ *
+ * @return bool
+ */
+ protected function objectExists($objectPath)
+ {
+ return $this->storage->if_object_exists($this->bucket, $objectPath);
+ }
+}
diff --git a/Imagine/Cache/Resolver/AwsS3Resolver.php b/Imagine/Cache/Resolver/AwsS3Resolver.php
new file mode 100644
index 000000000..7da87a89c
--- /dev/null
+++ b/Imagine/Cache/Resolver/AwsS3Resolver.php
@@ -0,0 +1,226 @@
+storage = $storage;
+
+ $this->bucket = $bucket;
+ $this->acl = $acl;
+
+ $this->objUrlOptions = $objUrlOptions;
+ }
+
+ /**
+ * Sets the logger to be used.
+ *
+ * @param LoggerInterface $logger
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param CacheManager $cacheManager
+ */
+ public function setCacheManager(CacheManager $cacheManager)
+ {
+ $this->cacheManager = $cacheManager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resolve(Request $request, $path, $filter)
+ {
+ $objectPath = $this->getObjectPath($path, $filter);
+ if ($this->objectExists($objectPath)) {
+ return new RedirectResponse($this->getObjectUrl($objectPath), 301);
+ }
+
+ return $objectPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ try {
+ $storageResponse = $this->storage->putObject(array(
+ 'ACL' => $this->acl,
+ 'Bucket' => $this->bucket,
+ 'Key' => $targetPath,
+ 'Body' => $response->getContent(),
+ 'ContentType' => $response->headers->get('Content-Type')
+ ));
+ } catch (\Exception $e) {
+ if ($this->logger) {
+ $this->logger->warn('The object could not be created on Amazon S3.', array(
+ 'targetPath' => $targetPath,
+ 'filter' => $filter,
+ ));
+ }
+
+ return $response;
+ }
+
+ $response->setStatusCode(301);
+ $response->headers->set('Location', $storageResponse->get('ObjectURL'));
+
+ return $response;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getBrowserPath($path, $filter, $absolute = false)
+ {
+ $objectPath = $this->getObjectPath($path, $filter);
+ if ($this->objectExists($objectPath)) {
+ return $this->getObjectUrl($objectPath);
+ }
+
+ return $this->cacheManager->generateUrl($path, $filter, $absolute);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove($targetPath, $filter)
+ {
+ if (!$this->objectExists($targetPath)) {
+ // A non-existing object to delete: done!
+ return true;
+ }
+
+ try {
+ $response = $this->storage->deleteObject(array(
+ 'Bucket' => $this->bucket,
+ 'Key' => $targetPath,
+ ));
+
+ return true;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets a single option to be passed when retrieving an objects URL.
+ *
+ * If the option is already set, it will be overwritten.
+ *
+ * @see Aws\S3\S3Client::getObjectUrl() for available options.
+ *
+ * @param string $key The name of the option.
+ * @param mixed $value The value to be set.
+ *
+ * @return AmazonS3Resolver $this
+ */
+ public function setObjectUrlOption($key, $value)
+ {
+ $this->objUrlOptions[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function clear($cachePrefix)
+ {
+ // TODO: implement cache clearing for Amazon S3 service
+ }
+
+ /**
+ * Returns the object path within the bucket.
+ *
+ * @param string $path The base path of the resource.
+ * @param string $filter The name of the imagine filter in effect.
+ *
+ * @return string The path of the object on S3.
+ */
+ protected function getObjectPath($path, $filter)
+ {
+ return str_replace('//', '/', $filter.'/'.$path);
+ }
+
+ /**
+ * Returns the URL for an object saved on Amazon S3.
+ *
+ * @param string $targetPath
+ *
+ * @return string
+ */
+ protected function getObjectUrl($targetPath)
+ {
+ return $this->storage->getObjectUrl($this->bucket, $targetPath, 0, $this->objUrlOptions);
+ }
+
+ /**
+ * Checks whether an object exists.
+ *
+ * @param string $objectPath
+ *
+ * @return bool
+ */
+ protected function objectExists($objectPath)
+ {
+ return $this->storage->doesObjectExist($this->bucket, $objectPath);
+ }
+}
diff --git a/Imagine/Cache/Resolver/CacheResolver.php b/Imagine/Cache/Resolver/CacheResolver.php
new file mode 100644
index 000000000..8bafec06b
--- /dev/null
+++ b/Imagine/Cache/Resolver/CacheResolver.php
@@ -0,0 +1,246 @@
+cache = $cache;
+ $this->resolver = $cacheResolver;
+
+ if (null === $optionsResolver) {
+ $optionsResolver = new OptionsResolver();
+ }
+
+ $this->setDefaultOptions($optionsResolver);
+ $this->options = $optionsResolver->resolve($options);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resolve(Request $request, $path, $filter)
+ {
+ $key = $this->generateCacheKey('resolve', $path, $filter);
+ if ($this->cache->contains($key)) {
+ return $this->cache->fetch($key);
+ }
+
+ $targetPath = $this->resolver->resolve($request, $path, $filter);
+ $this->saveToCache($key, $targetPath);
+
+ /*
+ * The targetPath being a string will be forwarded to the ResolverInterface::store method.
+ * As there is no way to reverse this operation by the interface, we store this information manually.
+ *
+ * If it's not a string, it's a Response it will be returned as it without calling the store method.
+ */
+ if (is_string($targetPath)) {
+ $reverseKey = $this->generateCacheKey('reverse', $targetPath, $filter);
+ $this->saveToCache($reverseKey, $path);
+ }
+
+ return $targetPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ return $this->resolver->store($response, $targetPath, $filter);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getBrowserPath($path, $filter, $absolute = false)
+ {
+ $key = $this->generateCacheKey('getBrowserPath', $path, $filter, array(
+ $absolute ? 'absolute' : 'relative',
+ ));
+
+ if ($this->cache->contains($key)) {
+ return $this->cache->fetch($key);
+ }
+
+ $result = $this->resolver->getBrowserPath($path, $filter, $absolute);
+ $this->saveToCache($key, $result);
+
+ return $result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove($targetPath, $filter)
+ {
+ $removed = $this->resolver->remove($targetPath, $filter);
+
+ // If the resolver did not remove the content, we can leave the cache.
+ if ($removed) {
+ $reverseKey = $this->generateCacheKey('reverse', $targetPath, $filter);
+ if ($this->cache->contains($reverseKey)) {
+ $path = $this->cache->fetch($reverseKey);
+
+ // The indexKey is not utilizing the method so the value is not important.
+ $indexKey = $this->generateIndexKey($this->generateCacheKey(null, $path, $filter));
+
+ // Retrieve the index and remove the content from the cache.
+ $index = $this->cache->fetch($indexKey);
+ foreach ($index as $eachCacheKey) {
+ $this->cache->delete($eachCacheKey);
+ }
+
+ // Remove the auxiliary keys.
+ $this->cache->delete($indexKey);
+ $this->cache->delete($reverseKey);
+ }
+ }
+
+ return $removed;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function clear($cachePrefix)
+ {
+ // TODO: implement cache clearing
+ }
+
+ /**
+ * Generate a unique cache key based on the given parameters.
+ *
+ * When overriding this method, ensure generateIndexKey is adjusted accordingly.
+ *
+ * @param string $method The cached method.
+ * @param string $path The image path in use.
+ * @param string $filter The filter in use.
+ * @param array $suffixes An optional list of additional parameters to use to create the key.
+ *
+ * @return string
+ */
+ public function generateCacheKey($method, $path, $filter, array $suffixes = array())
+ {
+ $keyStack = array(
+ $this->options['global_prefix'],
+ $this->options['prefix'],
+ $filter,
+ $path,
+ $method,
+ );
+
+ return implode('.', array_merge($keyStack, $suffixes));
+ }
+
+ /**
+ * Generate the index key for the given cacheKey.
+ *
+ * The index contains a list of cache keys related to an image and a filter.
+ *
+ * @param string $cacheKey
+ *
+ * @return string
+ */
+ protected function generateIndexKey($cacheKey)
+ {
+ $cacheKeyStack = explode('.', $cacheKey);
+
+ $indexKeyStack = array(
+ $this->options['global_prefix'],
+ $this->options['prefix'],
+ $this->options['index_key'],
+ $cacheKeyStack[2], // filter
+ $cacheKeyStack[3], // path
+ );
+
+ return implode('.', $indexKeyStack);
+ }
+
+ /**
+ * Save the given content to the cache and update the cache index.
+ *
+ * @param string $cacheKey
+ * @param mixed $content
+ *
+ * @return bool
+ */
+ protected function saveToCache($cacheKey, $content)
+ {
+ // Create or update the index list containing all cache keys for a given image and filter pairing.
+ $indexKey = $this->generateIndexKey($cacheKey);
+ if ($this->cache->contains($indexKey)) {
+ $index = $this->cache->fetch($indexKey);
+
+ if (!in_array($cacheKey, $index)) {
+ $index[] = $cacheKey;
+ }
+ } else {
+ $index = array($cacheKey);
+ }
+
+ /*
+ * Only save the content, if the index has been updated successfully.
+ * This is required to have a (hopefully) synchron state between cache and backend.
+ *
+ * "Hopefully" because there are caches (like Memcache) which will remove keys by themselves.
+ */
+ if ($this->cache->save($indexKey, $index)) {
+ return $this->cache->save($cacheKey, $content);
+ }
+
+ return false;
+ }
+
+ protected function setDefaultOptions(OptionsResolverInterface $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'global_prefix' => 'liip_imagine.resolver_cache',
+ 'prefix' => get_class($this->resolver),
+ 'index_key' => 'index',
+ ));
+
+ $resolver->setAllowedTypes(array(
+ 'global_prefix' => 'string',
+ 'prefix' => 'string',
+ 'index_key' => 'string',
+ ));
+ }
+}
diff --git a/Imagine/Cache/Resolver/NoCacheResolver.php b/Imagine/Cache/Resolver/NoCacheResolver.php
new file mode 100644
index 000000000..0a8ced5f9
--- /dev/null
+++ b/Imagine/Cache/Resolver/NoCacheResolver.php
@@ -0,0 +1,38 @@
+setBasePath($request->getBaseUrl());
+
+ return $this->getFilePath($path, $filter);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function store(Response $response, $targetPath, $filter)
+ {
+ return $response;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove($targetPath, $filter)
+ {
+ return true;
+ }
+}
diff --git a/Imagine/Cache/Resolver/ResolverInterface.php b/Imagine/Cache/Resolver/ResolverInterface.php
new file mode 100644
index 000000000..0b91b4d84
--- /dev/null
+++ b/Imagine/Cache/Resolver/ResolverInterface.php
@@ -0,0 +1,65 @@
+decodeBrowserPath($this->getBrowserPath($path, $filter));
+ $this->basePath = $request->getBaseUrl();
+ $targetPath = $this->getFilePath($path, $filter);
+
+ // if the file has already been cached, we're probably not rewriting
+ // correctly, hence make a 301 to proper location, so browser remembers
+ if (file_exists($targetPath)) {
+ // Strip the base URL of this request from the browserpath to not interfere with the base path.
+ $baseUrl = $request->getBaseUrl();
+ if ($baseUrl && 0 === strpos($browserPath, $baseUrl)) {
+ $browserPath = substr($browserPath, strlen($baseUrl));
+ }
+
+ return new RedirectResponse($request->getBasePath().$browserPath);
+ }
+
+ return $targetPath;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getBrowserPath($targetPath, $filter, $absolute = false)
+ {
+ return $this->cacheManager->generateUrl($targetPath, $filter, $absolute);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function clear($cachePrefix)
+ {
+ // Let's just avoid to remove the web/ directory content if cache prefix is empty
+ if ($cachePrefix === '') {
+ throw new \InvalidArgumentException("Cannot clear the Imagine cache because the cache_prefix is empty in your config.");
+ }
+
+ $cachePath = $this->cacheManager->getWebRoot() . $cachePrefix;
+
+ // Avoid an exception if the cache path does not exist (i.e. Imagine didn't yet render any image)
+ if (is_dir($cachePath)) {
+ $this->filesystem->remove(Finder::create()->in($cachePath)->depth(0)->directories());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function getFilePath($path, $filter)
+ {
+ $browserPath = $this->decodeBrowserPath($this->getBrowserPath($path, $filter));
+
+ if (!empty($this->basePath) && 0 === strpos($browserPath, $this->basePath)) {
+ $browserPath = substr($browserPath, strlen($this->basePath));
+ }
+
+ return $this->cacheManager->getWebRoot().$browserPath;
+ }
+
+ /**
+ * Decodes the URL encoded browser path.
+ *
+ * @param string $browserPath
+ *
+ * @return string
+ */
+ protected function decodeBrowserPath($browserPath)
+ {
+ //TODO: find out why I need double urldecode to get a valid path
+ return urldecode(urldecode($browserPath));
+ }
+}
diff --git a/Imagine/CachePathResolver.php b/Imagine/CachePathResolver.php
deleted file mode 100644
index 12402912f..000000000
--- a/Imagine/CachePathResolver.php
+++ /dev/null
@@ -1,124 +0,0 @@
-router = $router;
- $this->filesystem = $filesystem;
- $this->webRoot = realpath($webRoot);
- }
-
- /**
- * Gets filtered path for rendering in the browser
- *
- * @param string $path
- * @param string $filter
- * @param boolean $absolute
- *
- * @return string
- */
- public function getBrowserPath($targetPath, $filter, $absolute = false)
- {
- $params = array('path' => ltrim($targetPath, '/'));
-
- $path = str_replace(
- urlencode($params['path']),
- urldecode($params['path']),
- $this->router->generate('_imagine_'.$filter, $params, $absolute)
- );
-
- return $path;
- }
-
- /**
- * Resolves filtered path for rendering in the browser
- *
- * @param Request $request
- * @param string $path
- * @param string $filter
- *
- * @return string
- */
- public function resolve(Request $request, $targetPath, $filter)
- {
- //TODO: find out why I need double urldecode to get a valid path
- $browserPath = urldecode(urldecode($this->getBrowserPath($targetPath, $filter)));
-
- // if cache path cannot be determined, return 404
- if (null === $browserPath) {
- throw new NotFoundHttpException('Image doesn\'t exist');
- }
-
- $basePath = $request->getBaseUrl();
- if (!empty($basePath) && 0 === strpos($browserPath, $basePath)) {
- $browserPath = substr($browserPath, strlen($basePath));
- }
-
- $targetPath = $this->webRoot.$browserPath;
-
- // if the file has already been cached, we're probably not rewriting
- // correctly, hence make a 301 to proper location, so browser remembers
- if (file_exists($targetPath)) {
- return new RedirectResponse($request->getBasePath().$browserPath);
- }
-
- return $targetPath;
- }
-
- /**
- * @throws \RuntimeException
- * @param Response $response
- * @param string $targetPath
- *
- * @return Response
- */
- public function store(Response $response, $targetPath)
- {
- $dir = pathinfo($targetPath, PATHINFO_DIRNAME);
-
- if (!is_dir($dir) && !$this->filesystem->mkdir($dir)) {
- throw new \RuntimeException(sprintf(
- 'Could not create directory %s', $dir
- ));
- }
-
- file_put_contents($targetPath, $response->getContent());
-
- $response->setStatusCode(201);
-
- return $response;
- }
-}
diff --git a/Imagine/Data/DataManager.php b/Imagine/Data/DataManager.php
new file mode 100644
index 000000000..b6076fef1
--- /dev/null
+++ b/Imagine/Data/DataManager.php
@@ -0,0 +1,89 @@
+filterConfig = $filterConfig;
+ $this->defaultLoader = $defaultLoader;
+ }
+
+ /**
+ * Adds a loader to retrieve images for the given filter.
+ *
+ * @param string $filter
+ * @param LoaderInterface $loader
+ *
+ * @return void
+ */
+ public function addLoader($filter, LoaderInterface $loader)
+ {
+ $this->loaders[$filter] = $loader;
+ }
+
+ /**
+ * Returns a loader previously attached to the given filter.
+ *
+ * @param string $filter
+ *
+ * @return LoaderInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function getLoader($filter)
+ {
+ $config = $this->filterConfig->get($filter);
+
+ $loaderName = empty($config['data_loader'])
+ ? $this->defaultLoader : $config['data_loader'];
+
+ if (!isset($this->loaders[$loaderName])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Could not find data loader for "%s" filter type', $filter
+ ));
+ }
+
+ return $this->loaders[$loaderName];
+ }
+
+ /**
+ * Retrieves an image with the given filter applied.
+ *
+ * @param string $filter
+ * @param string $path
+ *
+ * @return \Imagine\Image\ImageInterface
+ */
+ public function find($filter, $path)
+ {
+ $loader = $this->getLoader($filter);
+
+ return $loader->find($path);
+ }
+}
diff --git a/Imagine/Data/Loader/AbstractDoctrineLoader.php b/Imagine/Data/Loader/AbstractDoctrineLoader.php
new file mode 100644
index 000000000..4113aaf48
--- /dev/null
+++ b/Imagine/Data/Loader/AbstractDoctrineLoader.php
@@ -0,0 +1,73 @@
+imagine = $imagine;
+ $this->manager = $manager;
+ $this->class = $class;
+ }
+
+ /**
+ * Map the requested path (ie. subpath in the URL) to an id that can be used to lookup the image in the Doctrine store.
+ *
+ * @param string $path
+ *
+ * @return string
+ */
+ abstract protected function mapPathToId($path);
+
+ /**
+ * Return a stream resource from the Doctrine entity/document with the image content
+ *
+ * @param object $image
+ *
+ * @return resource
+ */
+ abstract protected function getStreamFromImage($image);
+
+ /**
+ * @param string $path
+ *
+ * @return \Imagine\Image\ImageInterface
+ */
+ public function find($path)
+ {
+ $image = $this->manager->find($this->class, $this->mapPathToId($path));
+
+ if (!$image) {
+ throw new NotFoundHttpException(sprintf('Source image not found with id "%s"', $path));
+ }
+
+ return $this->imagine->load(stream_get_contents($this->getStreamFromImage($image)));
+ }
+}
diff --git a/Imagine/Data/Loader/DoctrinePHPCRLoader.php b/Imagine/Data/Loader/DoctrinePHPCRLoader.php
new file mode 100644
index 000000000..c56c91d0f
--- /dev/null
+++ b/Imagine/Data/Loader/DoctrinePHPCRLoader.php
@@ -0,0 +1,80 @@
+manager = $manager;
+ $this->class = $class;
+ $this->rootPath = $rootPath;
+ }
+
+ protected function getStreamFromImage($image)
+ {
+ return $image->getContent();
+ }
+
+ /**
+ * @param string $path
+ *
+ * @return \Imagine\Image\ImageInterface
+ */
+ public function find($path)
+ {
+ $file = $this->rootPath.'/'.ltrim($path, '/');
+ $info = $this->getFileInfo($file);
+ $name = $info['dirname'].'/'.$info['filename'];
+
+ // consider full path as provided (with or without an extension)
+ $paths = array($file);
+ foreach ($this->formats as $format) {
+ // consider all possible alternative extensions
+ if (empty($info['extension']) || $info['extension'] !== $format) {
+ $paths[] = $name.'.'.$format;
+ }
+ }
+
+ // if the full path contained an extension, also consider the full path without an extension
+ if ($file !== $name) {
+ $paths[] = $name;
+ }
+
+ $images = $this->manager->findMany($this->class, $paths);
+ if (!$images->count()) {
+ throw new NotFoundHttpException(sprintf('Source image not found with id "%s"', $path));
+ }
+
+ return $this->imagine->load(stream_get_contents($this->getStreamFromImage($images->first())));
+ }
+}
diff --git a/Imagine/Data/Loader/ExtendedFileSystemLoader.php b/Imagine/Data/Loader/ExtendedFileSystemLoader.php
new file mode 100644
index 000000000..aa0ec117a
--- /dev/null
+++ b/Imagine/Data/Loader/ExtendedFileSystemLoader.php
@@ -0,0 +1,47 @@
+transformers = $transformers;
+ }
+
+ /**
+ * Apply transformers to the file.
+ *
+ * @param $absolutePath
+ *
+ * @return array
+ */
+ protected function getFileInfo($absolutePath)
+ {
+ if (!empty($this->transformers)) {
+ foreach ($this->transformers as $transformer) {
+ $absolutePath = $transformer->apply($absolutePath);
+ }
+ }
+
+ return pathinfo($absolutePath);
+ }
+}
diff --git a/Imagine/Data/Loader/FileSystemLoader.php b/Imagine/Data/Loader/FileSystemLoader.php
new file mode 100644
index 000000000..e6ec344d9
--- /dev/null
+++ b/Imagine/Data/Loader/FileSystemLoader.php
@@ -0,0 +1,98 @@
+imagine = $imagine;
+ $this->formats = $formats;
+ $this->rootPath = realpath($rootPath);
+ }
+
+ /**
+ * Get the file info for the given path.
+ *
+ * This can optionally be used to generate the given file.
+ *
+ * @param string $absolutePath
+ *
+ * @return array
+ */
+ protected function getFileInfo($absolutePath)
+ {
+ return pathinfo($absolutePath);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function find($path)
+ {
+ if (false !== strpos($path, '/../') || 0 === strpos($path, '../')) {
+ throw new NotFoundHttpException(sprintf("Source image was searched with '%s' out side of the defined root path", $path));
+ }
+
+ $file = $this->rootPath.'/'.ltrim($path, '/');
+ $info = $this->getFileInfo($file);
+ $absolutePath = $info['dirname'].DIRECTORY_SEPARATOR.$info['basename'];
+
+ $name = $info['dirname'].DIRECTORY_SEPARATOR.$info['filename'];
+
+ $targetFormat = null;
+ // set a format if an extension is found and is allowed
+ if (isset($info['extension'])
+ && (empty($this->formats) || in_array($info['extension'], $this->formats))
+ ) {
+ $targetFormat = $info['extension'];
+ }
+
+ if (empty($targetFormat) || !file_exists($absolutePath)) {
+ // attempt to determine path and format
+ $absolutePath = null;
+ foreach ($this->formats as $format) {
+ if ($targetFormat !== $format && file_exists($name.'.'.$format)) {
+ $absolutePath = $name.'.'.$format;
+
+ break;
+ }
+ }
+
+ if (!$absolutePath) {
+ if (!empty($targetFormat) && is_file($name)) {
+ $absolutePath = $name;
+ } else {
+ throw new NotFoundHttpException(sprintf('Source image not found in "%s"', $file));
+ }
+ }
+ }
+
+ return $this->imagine->open($absolutePath);
+ }
+}
diff --git a/Imagine/Data/Loader/GridFSLoader.php b/Imagine/Data/Loader/GridFSLoader.php
new file mode 100644
index 000000000..7716bbbc6
--- /dev/null
+++ b/Imagine/Data/Loader/GridFSLoader.php
@@ -0,0 +1,57 @@
+imagine = $imagine;
+ $this->dm = $dm;
+ $this->class = $class;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function find($id)
+ {
+ $image = $this->dm
+ ->getRepository($this->class)
+ ->findAll()
+ ->getCollection()
+ ->findOne(array("_id" => new \MongoId($id)));
+
+ if (!$image) {
+ throw new NotFoundHttpException(sprintf('Source image not found with id "%s"', $id));
+ }
+
+ return $this->imagine->load($image['file']->getBytes());
+ }
+}
diff --git a/Imagine/Data/Loader/LoaderInterface.php b/Imagine/Data/Loader/LoaderInterface.php
new file mode 100644
index 000000000..406077d92
--- /dev/null
+++ b/Imagine/Data/Loader/LoaderInterface.php
@@ -0,0 +1,17 @@
+imagine = $imagine;
+ $this->wrapperPrefix = $wrapperPrefix;
+
+ if ($context && !is_resource($context)) {
+ throw new \InvalidArgumentException('The given context is no valid resource.');
+ }
+
+ $this->context = $context;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function find($path)
+ {
+ $name = $this->wrapperPrefix.$path;
+
+ /*
+ * This looks strange, but at least in PHP 5.3.8 it will raise an E_WARNING if the 4th parameter is null.
+ * fopen() will be called only once with the correct arguments.
+ *
+ * The error suppression is solely to determine whether the file exists.
+ * file_exists() is not used as not all wrappers support stat() to actually check for existing resources.
+ */
+ if (($this->context && !$resource = @fopen($name, 'r', null, $this->context)) || !$resource = @fopen($name, 'r')) {
+ throw new NotFoundHttpException('Source image not found.');
+ }
+
+ // Closing the opened stream to avoid locking of the resource to find.
+ fclose($resource);
+
+ return $this->imagine->load(file_get_contents($name, null, $this->context));
+ }
+}
diff --git a/Imagine/Data/Transformer/PdfTransformer.php b/Imagine/Data/Transformer/PdfTransformer.php
new file mode 100644
index 000000000..5d0e2acaa
--- /dev/null
+++ b/Imagine/Data/Transformer/PdfTransformer.php
@@ -0,0 +1,38 @@
+imagick = $imagick;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function apply($absolutePath)
+ {
+ $info = pathinfo($absolutePath);
+
+ if (isset($info['extension']) && false !== strpos(strtolower($info['extension']), 'pdf')) {
+ // If it doesn't exists, extract the first page of the PDF
+ if (!file_exists("$absolutePath.png")) {
+ $this->imagick->readImage($absolutePath.'[0]');
+ $this->imagick->setImageFormat('png');
+ $this->imagick->writeImage("$absolutePath.png");
+ $this->imagick->clear();
+ }
+
+ $absolutePath .= '.png';
+ }
+
+ return $absolutePath;
+ }
+}
diff --git a/Imagine/Data/Transformer/TransformerInterface.php b/Imagine/Data/Transformer/TransformerInterface.php
new file mode 100644
index 000000000..fd7097ec7
--- /dev/null
+++ b/Imagine/Data/Transformer/TransformerInterface.php
@@ -0,0 +1,15 @@
+imagine = $imagine;
- $this->formats = $formats;
- $this->webRoot = realpath($webRoot);
- }
-
- /**
- * @param string $path
- *
- * @return Imagine\Image\ImageInterface
- */
- public function find($path)
- {
- $path = $this->webRoot.'/'.ltrim($path, '/');;
-
- $info = pathinfo($path);
-
- $name = $info['dirname'].'/'.$info['filename'];
- $targetFormat = empty($this->formats) || in_array($info['extension'], $this->formats)
- ? $info['extension'] : null;
-
- if (empty($targetFormat) || !file_exists($path)) {
- // attempt to determine path and format
- $path = null;
- foreach ($this->formats as $format) {
- if ($targetFormat !== $format
- && file_exists($name.'.'.$format)
- ) {
- $path = $name.'.'.$format;
- break;
- }
- }
-
- if (!$path) {
- throw new NotFoundHttpException(sprintf('Source image not found in "%s"', $path));
- }
- }
-
- return $this->imagine->open($path);
- }
-}
diff --git a/Imagine/DataLoader/LoaderInterface.php b/Imagine/DataLoader/LoaderInterface.php
deleted file mode 100644
index cb39780f2..000000000
--- a/Imagine/DataLoader/LoaderInterface.php
+++ /dev/null
@@ -1,13 +0,0 @@
-filters = $filters;
+ }
+
+ /**
+ * Gets a previously configured filter.
+ *
+ * @param string $filter
+ *
+ * @return array
+ *
+ * @throws \RuntimeException
+ */
+ public function get($filter)
+ {
+ if (empty($this->filters[$filter])) {
+ throw new \RuntimeException('Filter not defined: '.$filter);
+ }
+
+ return $this->filters[$filter];
+ }
+
+ /**
+ * Sets a configuration on the given filter.
+ *
+ * @param string $filter
+ * @param array $config
+ *
+ * @return array
+ */
+ public function set($filter, array $config)
+ {
+ return $this->filters[$filter] = $config;
+ }
+}
diff --git a/Imagine/Filter/FilterManager.php b/Imagine/Filter/FilterManager.php
index 3e050f4e9..a678aa72c 100644
--- a/Imagine/Filter/FilterManager.php
+++ b/Imagine/Filter/FilterManager.php
@@ -2,83 +2,72 @@
namespace Liip\ImagineBundle\Imagine\Filter;
-use Symfony\Component\HttpFoundation\Request,
- Symfony\Component\HttpFoundation\Response;
-
+use Imagine\Image\ImageInterface;
use Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
class FilterManager
{
/**
- * @var array
+ * @var FilterConfiguration
*/
- private $filters;
+ protected $filterConfig;
/**
- * @var array
+ * @var LoaderInterface[]
*/
- private $loaders;
+ protected $loaders = array();
/**
- * @param array $filters
+ * Constructor.
+ *
+ * @param FilterConfiguration $filterConfig
*/
- public function __construct(array $filters = array())
+ public function __construct(FilterConfiguration $filterConfig)
{
- $this->filters = $filters;
- $this->loaders = array();
+ $this->filterConfig = $filterConfig;
}
/**
- * @param $name
- * @param Loader\LoaderInterface $loader
- *
+ * Adds a loader to handle the given filter.
+ *
+ * @param string $filter
+ * @param LoaderInterface $loader
+ *
* @return void
*/
- public function addLoader($name, LoaderInterface $loader)
+ public function addLoader($filter, LoaderInterface $loader)
{
- $this->loaders[$name] = $loader;
+ $this->loaders[$filter] = $loader;
}
/**
- * @param $filter
- *
- * @return array
+ * @return FilterConfiguration
*/
- public function getFilterConfig($filter)
+ public function getFilterConfiguration()
{
- if (empty($this->filters[$filter])) {
- new \RuntimeException('Filter not defined: '.$filter);
- }
-
- return $this->filters[$filter];
+ return $this->filterConfig;
}
/**
+ * Returns a response containing the given image after applying the given filter on it.
+ *
+ * @uses FilterManager::applyFilterSet
+ *
* @param Request $request
- * @param $filter
- * @param $image
+ * @param string $filter
+ * @param ImageInterface $image
* @param string $localPath
*
* @return Response
*/
- public function get(Request $request, $filter, $image, $localPath)
+ public function get(Request $request, $filter, ImageInterface $image, $localPath)
{
- if (!isset($this->filters[$filter])) {
- throw new \InvalidArgumentException(sprintf(
- 'Could not find image filter "%s"', $filter
- ));
- }
-
- $config = $this->filters[$filter];
+ $config = $this->getFilterConfiguration()->get($filter);
- foreach ($config['filters'] as $filter => $options) {
- if (!isset($this->loaders[$filter])) {
- throw new \InvalidArgumentException(sprintf(
- 'Could not find loader for "%s" filter type', $filter
- ));
- }
- $image = $this->loaders[$filter]->load($image, $options);
- }
+ $image = $this->applyFilter($image, $filter);
if (empty($config['format'])) {
$format = pathinfo($localPath, PATHINFO_EXTENSION);
@@ -98,4 +87,31 @@ public function get(Request $request, $filter, $image, $localPath)
return new Response($image, 200, array('Content-Type' => $contentType));
}
+
+ /**
+ * Apply the provided filter set on the given Image.
+ *
+ * @param ImageInterface $image
+ * @param string $filter
+ *
+ * @return ImageInterface
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function applyFilter(ImageInterface $image, $filter)
+ {
+ $config = $this->getFilterConfiguration()->get($filter);
+
+ foreach ($config['filters'] as $eachFilter => $eachOptions) {
+ if (!isset($this->loaders[$eachFilter])) {
+ throw new \InvalidArgumentException(sprintf(
+ 'Could not find filter loader for "%s" filter type', $eachFilter
+ ));
+ }
+
+ $image = $this->loaders[$eachFilter]->load($image, $eachOptions);
+ }
+
+ return $image;
+ }
}
diff --git a/Imagine/Filter/Loader/BackgroundFilterLoader.php b/Imagine/Filter/Loader/BackgroundFilterLoader.php
new file mode 100644
index 000000000..d787cbdc8
--- /dev/null
+++ b/Imagine/Filter/Loader/BackgroundFilterLoader.php
@@ -0,0 +1,27 @@
+imagine = $imagine;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ $background = new Color(isset($options['color']) ? $options['color'] : '#fff');
+ $topLeft = new Point(0, 0);
+ $canvas = $this->imagine->create($image->getSize(), $background);
+
+ return $canvas->paste($image, $topLeft);
+ }
+}
diff --git a/Imagine/Filter/Loader/CropFilterLoader.php b/Imagine/Filter/Loader/CropFilterLoader.php
new file mode 100644
index 000000000..7c9a72093
--- /dev/null
+++ b/Imagine/Filter/Loader/CropFilterLoader.php
@@ -0,0 +1,25 @@
+apply($image);
+
+ return $image;
+ }
+}
diff --git a/Imagine/Filter/Loader/LoaderInterface.php b/Imagine/Filter/Loader/LoaderInterface.php
index b834ae5d0..5deff85a0 100644
--- a/Imagine/Filter/Loader/LoaderInterface.php
+++ b/Imagine/Filter/Loader/LoaderInterface.php
@@ -7,10 +7,12 @@
interface LoaderInterface
{
/**
- * @param Imagine\Image\ImagineInterface $image
+ * Loads and applies a filter on the given image.
+ *
+ * @param ImageInterface $image
* @param array $options
*
- * @return Imagine\Image\ImageInterface
+ * @return ImageInterface
*/
function load(ImageInterface $image, array $options = array());
}
diff --git a/Imagine/Filter/Loader/PasteFilterLoader.php b/Imagine/Filter/Loader/PasteFilterLoader.php
new file mode 100644
index 000000000..bb4f06d1a
--- /dev/null
+++ b/Imagine/Filter/Loader/PasteFilterLoader.php
@@ -0,0 +1,27 @@
+imagine = $imagine;
+ $this->rootPath = $rootPath;
+ }
+
+ /**
+ * @see Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface::load()
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ list($x, $y) = $options['start'];
+ $destImage = $this->imagine->open($this->rootPath.'/'.$options['image']);
+
+ return $image->paste($destImage, new Point($x, $y));
+ }
+}
diff --git a/Imagine/Filter/Loader/RelativeResizeFilterLoader.php b/Imagine/Filter/Loader/RelativeResizeFilterLoader.php
new file mode 100644
index 000000000..88ad64a5f
--- /dev/null
+++ b/Imagine/Filter/Loader/RelativeResizeFilterLoader.php
@@ -0,0 +1,30 @@
+
+*/
+class RelativeResizeFilterLoader implements LoaderInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ if (list($method, $parameter) = each($options)) {
+ $filter = new RelativeResize($method, $parameter);
+
+ return $filter->apply($image);
+ }
+
+ throw new InvalidArgumentException('Expected method/parameter pair, none given');
+ }
+}
diff --git a/Imagine/Filter/Loader/ResizeFilterLoader.php b/Imagine/Filter/Loader/ResizeFilterLoader.php
new file mode 100644
index 000000000..6ac2b9a55
--- /dev/null
+++ b/Imagine/Filter/Loader/ResizeFilterLoader.php
@@ -0,0 +1,27 @@
+
+ */
+class ResizeFilterLoader implements LoaderInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ list($width, $height) = $options['size'];
+
+ $filter = new Resize(new Box($width, $height));
+
+ return $filter->apply($image);
+ }
+}
diff --git a/Imagine/Filter/Loader/StripFilterLoader.php b/Imagine/Filter/Loader/StripFilterLoader.php
new file mode 100644
index 000000000..e6509550e
--- /dev/null
+++ b/Imagine/Filter/Loader/StripFilterLoader.php
@@ -0,0 +1,17 @@
+apply($image);
+
+ return $image;
+ }
+}
diff --git a/Imagine/Filter/Loader/ThumbnailFilterLoader.php b/Imagine/Filter/Loader/ThumbnailFilterLoader.php
index efdc6a22e..66020cbdc 100644
--- a/Imagine/Filter/Loader/ThumbnailFilterLoader.php
+++ b/Imagine/Filter/Loader/ThumbnailFilterLoader.php
@@ -2,23 +2,29 @@
namespace Liip\ImagineBundle\Imagine\Filter\Loader;
-use Imagine\Image\Box;
use Imagine\Filter\Basic\Thumbnail;
+use Imagine\Image\Box;
use Imagine\Image\ImageInterface;
class ThumbnailFilterLoader implements LoaderInterface
{
+ /**
+ * {@inheritDoc}
+ */
public function load(ImageInterface $image, array $options = array())
{
- $mode = $options['mode'] === 'inset' ?
- ImageInterface::THUMBNAIL_INSET :
- ImageInterface::THUMBNAIL_OUTBOUND;
+ $mode = ImageInterface::THUMBNAIL_OUTBOUND;
+ if (!empty($options['mode']) && 'inset' === $options['mode']) {
+ $mode = ImageInterface::THUMBNAIL_INSET;
+ }
+
list($width, $height) = $options['size'];
$size = $image->getSize();
$origWidth = $size->getWidth();
$origHeight = $size->getHeight();
+
if (null === $width || null === $height) {
if (null === $height) {
$height = (int)(($width / $origWidth) * $origHeight);
@@ -27,8 +33,8 @@ public function load(ImageInterface $image, array $options = array())
}
}
- if ((!empty($options['allow_upscale']) && $origWidth !== $width && $origHeight !== $height)
- || ($origWidth > $width || $origHeight > $height)
+ if (($origWidth > $width || $origHeight > $height)
+ || (!empty($options['allow_upscale']) && ($origWidth !== $width || $origHeight !== $height))
) {
$filter = new Thumbnail(new Box($width, $height), $mode);
$image = $filter->apply($image);
diff --git a/Imagine/Filter/Loader/UpscaleFilterLoader.php b/Imagine/Filter/Loader/UpscaleFilterLoader.php
new file mode 100644
index 000000000..27730145a
--- /dev/null
+++ b/Imagine/Filter/Loader/UpscaleFilterLoader.php
@@ -0,0 +1,46 @@
+
+ */
+class UpscaleFilterLoader implements LoaderInterface
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ if (!isset($options['min'])) {
+ throw new InvalidArgumentException('Missing min option.');
+ }
+
+ list($width, $height) = $options['min'];
+
+ $size = $image->getSize();
+ $origWidth = $size->getWidth();
+ $origHeight = $size->getHeight();
+
+ if ($origWidth < $width || $origHeight < $height) {
+
+ $widthRatio = $width / $origWidth ;
+ $heightRatio = $height / $origHeight;
+
+ $ratio = $widthRatio > $heightRatio ? $widthRatio : $heightRatio;
+
+ $filter = new Resize(new Box($origWidth * $ratio, $origHeight * $ratio));
+
+ return $filter->apply($image);
+ }
+
+ return $image;
+ }
+}
diff --git a/Imagine/Filter/Loader/WatermarkFilterLoader.php b/Imagine/Filter/Loader/WatermarkFilterLoader.php
new file mode 100644
index 000000000..2a5ca4898
--- /dev/null
+++ b/Imagine/Filter/Loader/WatermarkFilterLoader.php
@@ -0,0 +1,93 @@
+imagine = $imagine;
+ $this->rootPath = $rootPath;
+ }
+
+ /**
+ * @see Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface::load()
+ */
+ public function load(ImageInterface $image, array $options = array())
+ {
+ $options += array(
+ 'size' => null,
+ 'position' => 'center'
+ );
+
+ if (substr($options['size'], -1) == '%') {
+ $options['size'] = substr($options['size'], 0, -1) / 100;
+ }
+
+ $watermark = $this->imagine->open($this->rootPath . '/' . $options['image']);
+
+ $size = $image->getSize();
+ $watermarkSize = $watermark->getSize();
+
+ // If 'null': Downscale if needed
+ if (!$options['size'] && ($size->getWidth() < $watermarkSize->getWidth() || $size->getHeight() < $watermarkSize->getHeight())) {
+ $options['size'] = 1.0;
+ }
+
+ if ($options['size']) {
+ $factor = $options['size'] * min($size->getWidth() / $watermarkSize->getWidth(), $size->getHeight() / $watermarkSize->getHeight());
+
+ $watermark->resize(new Box($watermarkSize->getWidth() * $factor, $watermarkSize->getHeight() * $factor));
+ $watermarkSize = $watermark->getSize();
+ }
+
+ switch ($options['position']) {
+ case 'topleft':
+ $x = 0;
+ $y = 0;
+ break;
+ case 'top':
+ $x = ($size->getWidth() - $watermarkSize->getWidth()) / 2;
+ $y = 0;
+ break;
+ case 'topright':
+ $x = $size->getWidth() - $watermarkSize->getWidth();
+ $y = 0;
+ break;
+ case 'left':
+ $x = 0;
+ $y = ($size->getHeight() - $watermarkSize->getHeight()) / 2;
+ break;
+ case 'center':
+ $x = ($size->getWidth() - $watermarkSize->getWidth()) / 2;
+ $y = ($size->getHeight() - $watermarkSize->getHeight()) / 2;
+ break;
+ case 'right':
+ $x = $size->getWidth() - $watermarkSize->getWidth();
+ $y = ($size->getHeight() - $watermarkSize->getHeight()) / 2;
+ break;
+ case 'bottomleft':
+ $x = 0;
+ $y = $size->getHeight() - $watermarkSize->getHeight();
+ break;
+ case 'bottom':
+ $x = ($size->getWidth() - $watermarkSize->getWidth()) / 2;
+ $y = $size->getHeight() - $watermarkSize->getHeight();
+ break;
+ case 'bottomright':
+ $x = $size->getWidth() - $watermarkSize->getWidth();
+ $y = $size->getHeight() - $watermarkSize->getHeight();
+ break;
+ default:
+ throw new \InvalidArgumentException("Unexpected position '{$options['position']}'");
+ break;
+ }
+
+ return $image->paste($watermark, new Point($x, $y));
+ }
+}
diff --git a/Imagine/Filter/RelativeResize.php b/Imagine/Filter/RelativeResize.php
new file mode 100644
index 000000000..14ebd4af9
--- /dev/null
+++ b/Imagine/Filter/RelativeResize.php
@@ -0,0 +1,42 @@
+
+ */
+class RelativeResize implements FilterInterface
+{
+ private $method;
+ private $parameter;
+
+ /**
+ * Constructs a RelativeResize filter with the given method and argument.
+ *
+ * @param string $method BoxInterface method
+ * @param mixed $parameter Parameter for BoxInterface method
+ */
+ public function __construct($method, $parameter)
+ {
+ if (!in_array($method, array('heighten', 'increase', 'scale', 'widen'))) {
+ throw new InvalidArgumentException(sprintf('Unsupported method: ', $method));
+ }
+
+ $this->method = $method;
+ $this->parameter = $parameter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function apply(ImageInterface $image)
+ {
+ return $image->resize(call_user_func(array($image->getSize(), $this->method), $this->parameter));
+ }
+}
diff --git a/LiipImagineBundle.php b/LiipImagineBundle.php
index c0ed338b3..70db038bf 100644
--- a/LiipImagineBundle.php
+++ b/LiipImagineBundle.php
@@ -3,13 +3,14 @@
namespace Liip\ImagineBundle;
use Liip\ImagineBundle\DependencyInjection\Compiler\LoadersCompilerPass;
+
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class LiipImagineBundle extends Bundle
{
/**
- * @see Symfony\Component\HttpKernel\Bundle.Bundle::build()
+ * {@inheritDoc}
*/
public function build(ContainerBuilder $container)
{
diff --git a/README.md b/README.md
index 6440c2582..947332e37 100644
--- a/README.md
+++ b/README.md
@@ -18,71 +18,20 @@ This will perform the transformation called `thumbnail`, which you can define
to do a number of different things, such as resizing, cropping, drawing,
masking, etc.
-This bundle integrates the standalone PHP "[Imagine library](/avalanche123/Imagine)".
+This bundle integrates the standalone PHP "[Imagine library](https://github.com/avalanche123/Imagine)".
-## Installation
-
-To install this bundle, you'll need both the [Imagine library](/avalanche123/Imagine)
-and this bundle. Installation depends on how your project is setup:
-
-### Step 1: Installation
-
-Add the following lines to your ``deps`` file
-
-```
-[Imagine]
- git=http://github.com/avalanche123/Imagine.git
- target=imagine
- version=v0.2.0
-
-[LiipImagineBundle]
- git=http://github.com/liip/LiipImagineBundle.git
- target=bundles/Liip/ImagineBundle
-```
-
-Next, update your vendors by running:
-
-``` bash
-$ ./bin/vendors install
-```
-
-### Step 2: Configure the autoloader
-
-Add the following entries to your autoloader:
-
-``` php
-registerNamespaces(array(
- // ...
-
- 'Imagine' => __DIR__.'/../vendor/imagine/lib',
- 'Liip' => __DIR__.'/../vendor/bundles',
-));
-```
+[![Build Status](https://secure.travis-ci.org/liip/LiipImagineBundle.png)](http://travis-ci.org/liip/LiipImagineBundle)
+[![Total Downloads](https://poser.pugx.org/liip/imagine-bundle/downloads.png)](https://packagist.org/packages/liip/imagine-bundle)
+[![Latest Stable Version](https://poser.pugx.org/liip/imagine-bundle/v/stable.png)](https://packagist.org/packages/liip/imagine-bundle)
-### Step 3: Enable the bundle
-Finally, enable the bundle in the kernel:
+## Installation
-``` php
-filter('/relative/path/to/image.jpg', 'my_thumb') ?>" />
```
-Behind the scenes, the bundles apples the filter(s) to the image on the first
+Behind the scenes, the bundles applies the filter(s) to the image on the first
request and then caches the image to a similar path. On the next request,
the cached image would be served directly from the file system.
@@ -150,142 +99,88 @@ Note: Using the ``dev`` environment you might find that the images are not prope
using the template helper. This is likely caused by having ``intercept_redirect`` enabled in your
application configuration. To ensure that the images are rendered disable this option:
-
``` jinja
web_profiler:
intercept_redirects: false
```
-## Configuration
-
-The default configuration for the bundle looks like this:
-
-``` yaml
-liip_imagine:
- web_root: %kernel.root_dir%/../web
- cache_prefix: /media/cache
- cache: true
- loader: ~
- driver: gd
- formats: []
- filter_sets: []
-```
-
-There are several configuration options available:
-
- - `web_root` - must be the absolute path to you application's web root. This
- is used to determine where to put generated image files, so that apache
- will pick them up before handing the request to Symfony2 next time they
- are requested.
-
- default: `%kernel.root_dir%/../web`
-
- - `cache_prefix` - this is also used in the path for image generation, so
- as to not clutter your web root with cached images. For example by default,
- the images would be written to the `web/media/cache/` directory.
+## Filters
- default: `/media/cache`
+The LiipImagineBundle provides a set of built-in filters.
+You may easily roll your own filter, see [the filters chapter in the documentation](Resources/doc/filters.md).
- - `cache` - if to cache the generated image in the local file system
+## Using the controller as a service
- - `loader` - service id for a custom loader
+If you need to use the filters in a controller, you can just load `ImagineController.php` controller as a service and handle the response:
- default: null (which means the standard filesystem loader is used)
-
- - `driver` - one of the three drivers: `gd`, `imagick`, `gmagick`
-
- default: `gd`
-
- - `formats` - optional list of image formats to which images may be converted to.
-
- - `filter_sets` - specify the filter sets that you want to define and use
-
-Each filter set that you specify have the following options:
-
- - `filters` - determine the type of filter to be used (refer to *Filters* section for more information)
- and options that should be passed to the specific filter type
- - `path` - used in place of the filter name to determine the path in combination with the global `cache_prefix`
- - `quality` - override the default quality of 100 for the generated images
- - `format` - to hardcode the output format
-
-## Built-in Filters
+``` php
+class MyController extends Controller
+{
+ public function indexAction()
+ {
+ // RedirectResponse object
+ $imagemanagerResponse = $this->container
+ ->get('liip_imagine.controller')
+ ->filterAction(
+ $this->getRequest(),
+ 'uploads/foo.jpg', // original image you want to apply a filter to
+ 'my_thumb' // filter defined in config.yml
+ );
+
+ // string to put directly in the "src" of the tag
+ $cacheManager = $this->container->get('liip_imagine.cache.manager');
+ $srcPath = $cacheManager->getBrowserPath('uploads/foo.jpg', 'my_thumb');
+
+ // ..
+ }
+}
+```
-Currently, this bundles comes with just one built-in filter: `thumbnail`.
+In case you need to add more logic the recommended solution is to either extend `ImagineController.php` controller or take the code from that controller as a basis for your own controller.
-### The `thumbnail` filter
+## Outside the web root
-The thumbnail filter, as the name implies, performs a thumbnail transformation
-on your image. Configuration looks like this:
+When your setup requires your source images to live outside the web root, or if that's just the way you roll,
+you have to set the bundle's parameter `data_root` in the `config.yml` with the absolute path where your source images are
+located:
``` yaml
liip_imagine:
- filter_sets:
- my_thumb:
- filters:
- thumbnail: { size: [120, 90], mode: outbound }
+ data_root: /path/to/source/images/dir
```
-The `mode` can be either `outbound` or `inset`.
-
-## Load your Custom Filters
-
-The ImagineBundle allows you to load your own custom filter classes. The only
-requirement is that each filter loader implement the following interface:
-
- Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface
-
-To tell the bundle about your new filter loader, register it in the service
-container and apply the `liip_imagine.filter.loader` tag to it (example here in XML):
+Afterwards, you need to grant read access on Apache to access the images source directory. For achieving it you have
+to add the following directive to your project's vhost file:
``` xml
-
-
-
-```
-
-For more information on the service container, see the Symfony2
-[Service Container](http://symfony.com/doc/current/book/service_container.html) documentation.
-
-You can now reference and use your custom filter when defining filter sets you'd
-like to apply in your configuration:
+
+
-``` yaml
-liip_imagine:
- filter_sets:
- my_special_style:
- filters:
- my_custom_filter: { }
+ Alias /FavouriteAlias /path/to/source/images/dir
+
+ AllowOverride None
+ Allow from All
+
+
```
-For an example of a filter loader implementation, refer to
-`Liip\ImagineBundle\Imagine\Filter\Loader\ThumbnailFilterLoader`.
-
-## Custom image loaders
-
-The ImagineBundle allows you to add your custom image loader classes. The only
-requirement is that each data loader implement the following interface:
-
- Liip\ImagineBundle\Imagine\DataLoader\LoaderInterface
-
-To tell the bundle about your new filter loader, register it in the service
-container just like any other service:
+Another way would be placing the directive in a separate file living inside your project. For instance,
+you can create a file `app/config/apache/photos.xml` and add to the project's vhost the following directive:
``` xml
-
-
- %liip_imagine.formats%
-
+
+
+
+ Include "/path/to/your/project/app/config/apache/photos.xml"
+
```
-For more information on the service container, see the Symfony2
-[Service Container](http://symfony.com/doc/current/book/service_container.html) documentation.
+This way you keep the file along with your code and you are able to change your files directory access easily or create
+different environment-dependant configuration files.
-You can enable your custom data loader by adding it to the your configuration:
+Either way, once you have granted access on Apache to read the `data_root` files, the relative path of an image with this
+absolute path `/path/to/source/images/dir/logo.png` must be `/FavouriteAlias/logo.png` to be readable.
-``` yaml
-liip_imagine:
- loader: acme_imagine.loader.my_custom
-```
+## Documentation
-For an example of a filter loader implementation, refer to
-`Liip\ImagineBundle\Imagine\DataLoader\FileSystemLoader`.
+For more detailed information about the features of this bundle, please refer to [the documentation](Resources/doc/index.md).
diff --git a/Resources/config/cache_clearer.xml b/Resources/config/cache_clearer.xml
new file mode 100644
index 000000000..ad956c8d8
--- /dev/null
+++ b/Resources/config/cache_clearer.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ Liip\ImagineBundle\Imagine\Cache\CacheClearer
+
+
+
+
+
+
+
+ %liip_imagine.cache_prefix%
+
+
+
+
diff --git a/Resources/config/imagine.xml b/Resources/config/imagine.xml
index a7dfc219f..02edbb8c2 100644
--- a/Resources/config/imagine.xml
+++ b/Resources/config/imagine.xml
@@ -7,9 +7,10 @@
+ Liip\ImagineBundle\Imagine\Filter\FilterConfiguration
Liip\ImagineBundle\Imagine\Filter\FilterManager
- Liip\ImagineBundle\Imagine\CachePathResolver
- Liip\ImagineBundle\Imagine\DataLoader\FileSystemLoader
+ Liip\ImagineBundle\Imagine\Data\DataManager
+ Liip\ImagineBundle\Imagine\Cache\CacheManager
@@ -32,71 +33,169 @@
+ Liip\ImagineBundle\Imagine\Filter\Loader\RelativeResizeFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\ResizeFilterLoader
Liip\ImagineBundle\Imagine\Filter\Loader\ThumbnailFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\CropFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\PasteFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\WatermarkFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\StripFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\BackgroundFilterLoader
+ Liip\ImagineBundle\Imagine\Filter\Loader\UpscaleFilterLoader
+
+
+
+ Liip\ImagineBundle\Imagine\Data\Loader\FileSystemLoader
+ Liip\ImagineBundle\Imagine\Data\Loader\StreamLoader
+
+
+
+ Liip\ImagineBundle\Imagine\Cache\Resolver\WebPathResolver
+ Liip\ImagineBundle\Imagine\Cache\Resolver\NoCacheResolver
+
+
+
+ Liip\ImagineBundle\Form\Type\ImageType
-
+
-
-
-
- %liip_imagine.web_root%
+
+
-
- %liip_imagine.filter_sets%
+
+
+ %liip_imagine.data.loader.default%
-
-
- %liip_imagine.formats%
+
+
+
%liip_imagine.web_root%
+ %liip_imagine.cache.resolver.default%
+
+
+
+ %liip_imagine.filter_sets%
-
+
+
+ %liip_imagine.controller_action%
%liip_imagine.cache_prefix%
%liip_imagine.filter_sets%
-
-
-
+
+
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+ %kernel.root_dir%
+
+
+
+
+
+ %kernel.root_dir%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %liip_imagine.formats%
+ %liip_imagine.data_root%
+
+
+
+
+
+
+
+
+ %liip_imagine.cache.resolver.base_path%
+
+
+ %liip_imagine.cache_mkdir_mode%
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Resources/doc/cache-resolver/amazons3.md b/Resources/doc/cache-resolver/amazons3.md
new file mode 100644
index 000000000..98a2523b4
--- /dev/null
+++ b/Resources/doc/cache-resolver/amazons3.md
@@ -0,0 +1,86 @@
+# AmazonS3Resolver
+
+The AmazonS3Resolver requires the [aws-sdk-php](https://github.com/amazonwebservices/aws-sdk-for-php).
+
+You can add the SDK by adding those lines to your `deps` file.
+
+``` ini
+[aws-sdk]
+ git=git://github.com/amazonwebservices/aws-sdk-for-php.git
+```
+
+Afterwards, you only need to configure some information regarding your AWS account and the bucket.
+
+``` yaml
+parameters:
+ amazon_s3.key: 'your-aws-key'
+ amazon_s3.secret: 'your-aws-secret'
+ amazon_s3.bucket: 'your-bucket.example.com'
+```
+
+Now you can set up the services required:
+
+``` yaml
+services:
+ acme.amazon_s3:
+ class: AmazonS3
+ arguments:
+ -
+ key: %amazon_s3.key%
+ secret: %amazon_s3.secret%
+ # more S3 specific options, see \AmazonS3::__construct()
+
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+Now you are ready to use the `AmazonS3Resolver` by configuring the bundle.
+The following example will configure the resolver is default.
+
+``` yaml
+liip_imagine:
+ cache: 'amazon_s3'
+```
+
+If you want to use other buckets for other images, simply alter the parameter names and create additional services!
+
+## Object URL Options
+
+In order to make use of the object URL options, you can simply add a call to the service, to alter those options you need.
+
+``` yaml
+services:
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ calls:
+ # This calls $service->setObjectUrlOption('https', true);
+ - [ setObjectUrlOption, [ 'https', true ] ]
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+You can also use the constructor of the resolver to directly inject multiple options.
+
+``` yaml
+services:
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ - "public-read" # AmazonS3::ACL_PUBLIC (default)
+ - { https: true, torrent: true }
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+- [Back to cache resolvers](../cache-resolvers.md)
+- [Back to the index](../index.md)
diff --git a/Resources/doc/cache-resolver/aws_s3.md b/Resources/doc/cache-resolver/aws_s3.md
new file mode 100644
index 000000000..249cae39e
--- /dev/null
+++ b/Resources/doc/cache-resolver/aws_s3.md
@@ -0,0 +1,89 @@
+# AwsS3Resolver
+
+The AwsS3Resolver requires the [aws-sdk-php](https://github.com/aws/aws-sdk-php).
+
+You can add the SDK by adding those lines to your `deps` file.
+
+``` ini
+[aws-sdk]
+ git=git://github.com/aws/aws-sdk-php.git
+```
+
+Afterwards, you only need to configure some information regarding your AWS account and the bucket.
+
+``` yaml
+parameters:
+ amazon_s3.key: 'your-aws-key'
+ amazon_s3.secret: 'your-aws-secret'
+ amazon_s3.bucket: 'your-bucket.example.com'
+ amazon_s3.region: 'your-bucket-region'
+```
+
+Now you can set up the services required:
+
+``` yaml
+services:
+ acme.amazon_s3:
+ class: Aws\S3\S3Client
+ factory_class: Aws\S3\S3Client
+ factory_method: factory
+ arguments:
+ -
+ key: %amazon_s3.key%
+ secret: %amazon_s3.secret%
+ region: %amazon_s3.region%
+
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AwsS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+Now you are ready to use the `AwsS3Resolver` by configuring the bundle.
+The following example will configure the resolver is default.
+
+``` yaml
+liip_imagine:
+ cache: 'amazon_s3'
+```
+
+If you want to use other buckets for other images, simply alter the parameter names and create additional services!
+
+## Object URL Options
+
+In order to make use of the object URL options, you can simply add a call to the service, to alter those options you need.
+
+``` yaml
+services:
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AwsS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ calls:
+ # This calls $service->setObjectUrlOption('Scheme', 'https');
+ - [ setObjectUrlOption, [ 'Scheme', 'https' ] ]
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+You can also use the constructor of the resolver to directly inject multiple options.
+
+``` yaml
+services:
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AwsS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+ - "public-read" # Aws\S3\Enum\CannedAcl::PUBLIC_READ (default)
+ - { Scheme: https }
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'amazon_s3' }
+```
+
+- [Back to cache resolvers](../cache-resolvers.md)
+- [Back to the index](../index.md)
diff --git a/Resources/doc/cache-resolver/cache.md b/Resources/doc/cache-resolver/cache.md
new file mode 100644
index 000000000..75d1b68eb
--- /dev/null
+++ b/Resources/doc/cache-resolver/cache.md
@@ -0,0 +1,61 @@
+# CacheResolver
+
+The `CacheResolver` requires the [Doctrine Cache](https://github.com/doctrine/cache).
+
+This resolver wraps another resolver around a `Cache`.
+
+Now you can set up the services required; by example using the `AmazonS3Resolver`.
+
+``` yaml
+services:
+ acme.amazon_s3:
+ class: AmazonS3
+ arguments:
+ -
+ key: %amazon_s3.key%
+ secret: %amazon_s3.secret%
+
+ acme.imagine.cache.resolver.amazon_s3:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver
+ arguments:
+ - "@acme.amazon_s3"
+ - "%amazon_s3.bucket%"
+
+ memcache:
+ class: Memcache
+ calls:
+ - [ 'connect', [ '127.0.0.1', 11211 ] ]
+
+ cache.memcache:
+ class: Doctrine\Common\Cache\MemcacheCache
+ calls:
+ - [ 'setMemcache', [ '@memcache' ] ]
+
+ # The actual
+ acme.imagine.cache.resolver.amazon_s3.cache:
+ class: Liip\ImagineBundle\Imagine\Cache\Resolver\CacheResolver
+ arguments:
+ - "@cache.memcache"
+ - "@acme.imagine.cache.resolver.amazon_s3"
+ -
+ prefix: "amazon_s3"
+ tags:
+ - { name: 'liip_imagine.cache.resolver', resolver: 'cached_amazon_s3' }
+```
+
+There are currently three options available when configuring the `CacheResolver`:
+
+* `global_prefix` A prefix for all keys within the cache. This is useful to avoid colliding keys when using the same cache for different systems.
+* `prefix` A "local" prefix for this wrapper. This is useful when re-using the same resolver for multiple filters. This mainly affects the clear method.
+* `index_key` The name of the index key being used to save a list of created cache keys regarding one image and filter pairing.
+
+Now you are ready to use the `CacheResolver` by configuring the bundle.
+The following example will configure the resolver is default.
+
+``` yaml
+liip_imagine:
+ cache: 'cached_amazon_s3'
+```
+
+- [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
new file mode 100644
index 000000000..d3c2ceff1
--- /dev/null
+++ b/Resources/doc/cache-resolvers.md
@@ -0,0 +1,58 @@
+# Built-In CacheResolver
+
+* [AmazonS3](cache-resolver/amazons3.md)
+* [AwsS3](cache-resolver/aws_s3.md) - for SDK version 2
+* [CacheResolver](cache-resolver/cache.md)
+
+# Custom cache resolver
+
+The ImagineBundle allows you to add your custom cache resolver classes. The only
+requirement is that each cache resolver loader implement the following interface:
+
+ Liip\ImagineBundle\Imagine\Cache\Resolver\ResolverInterface
+
+To tell the bundle about your new cache resolver, register it in the service
+container and apply the `liip_imagine.cache.resolver` tag to it (example here in XML):
+
+``` xml
+
+
+
+
+ %liip_imagine.web_root%
+
+```
+
+For more information on the service container, see the Symfony2
+[Service Container](http://symfony.com/doc/current/book/service_container.html) documentation.
+
+You can set your custom cache reslover by adding it to the your configuration as the new
+default resolver as follows:
+
+``` yaml
+liip_imagine:
+ cache: my_custom_cache
+```
+
+Alternatively you can only set the custom cache resolver for just a specific filter set:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_special_style:
+ cache: my_custom_cache
+ filters:
+ my_custom_filter: { }
+```
+
+For an example of a cache resolver implementation, refer to
+`Liip\ImagineBundle\Imagine\Cache\Resolver\WebPathResolver`.
+
+## CacheClearer
+
+Custom cache resolver classes must implement the ```clear``` method, at worst doing nothing.
+
+When the ```console cache:clear``` command is run, the clear method of all the registered cache
+resolvers is automatically called.
+
+[Back to the index](index.md)
diff --git a/Resources/doc/configuration.md b/Resources/doc/configuration.md
new file mode 100644
index 000000000..57a7c9332
--- /dev/null
+++ b/Resources/doc/configuration.md
@@ -0,0 +1,100 @@
+# Configuration
+
+The default configuration for the bundle looks like this:
+
+``` yaml
+liip_imagine:
+ driver: gd
+ web_root: %kernel.root_dir%/../web
+ data_root: %liip_imagine.web_root%
+ cache_mkdir_mode: 0777
+ cache_prefix: /media/cache
+ cache: web_path
+ cache_clearer: true
+ data_loader: filesystem
+ controller_action: liip_imagine.controller:filterAction
+ formats: []
+ filter_sets:
+
+ # Prototype
+ name:
+ path: ~
+ quality: 100
+ format: ~
+ cache: ~
+ data_loader: ~
+ controller_action: ~
+ route: []
+ filters:
+
+ # Prototype
+ name: []
+```
+
+There are several configuration options available:
+
+ - `web_root` - must be the absolute path to you application's web root. This
+ is used to determine where to put generated image files, so that apache
+ will pick them up before handing the request to Symfony2 next time they
+ are requested.
+
+ default: `%kernel.root_dir%/../web`
+
+ - `data_root` - the absolute path to the location that original files should
+ be sourced from. This option only changes the standard filesystem loader.
+
+ default: `%kernel.root_dir%/../web`
+
+ - `cache_mkdir_mode` - permissions to set on generated cache directories.
+ Must be specified as an octal number, which means it should begin with a
+ leading zero. mode is ignored on Windows.
+
+ default: `0777`
+
+ - `cache_prefix` - this is also used in the path for image generation, so
+ as to not clutter your web root with cached images. For example by default,
+ the images would be written to the `web/media/cache/` directory.
+
+ default: `/media/cache`
+
+ - `cache` - default cache resolver
+
+ default: web_path (which means the standard web_path resolver is used)
+
+ - `cache_clearer` - Whether or not to clear the image cache when the `kernel.cache_clearer` event occurs.
+ This option doesn't have any effect in symfony < 2.1
+
+ default: true
+
+ - `data_loader` - name of a custom data loader
+
+ default: filesystem (which means the standard filesystem loader is used)
+
+ - `controller_action` - name of the controller action to use in the route loader
+
+ default: liip_imagine.controller:filterAction
+
+ - `driver` - one of the three drivers: `gd`, `imagick`, `gmagick`
+
+ default: `gd`
+
+ - `formats` - optional list of image formats to which images may be converted to.
+
+ - `filter_sets` - specify the filter sets that you want to define and use
+
+Each filter set that you specify has the following options:
+
+ - `filters` - determine the type of filter to be used (refer to *Filters* section for more information)
+ and options that should be passed to the specific filter type
+ - `path` - used in place of the filter name to determine the path in combination with the global `cache_prefix`
+ - `quality` - override the default quality of 100 for the generated images
+ - `cache` - override the default cache setting
+ - `data_loader` - override the default data loader
+ - `controller_action` - override the default controller action
+ - `route` - optional list of route requirements, defaults and options using in the route loader. Add array with keys 'requirements', 'defaults' or 'options'.
+
+ default: empty array
+
+ - `format` - hardcodes the output format (aka the requested format is ignored)
+
+[Back to the index](index.md)
diff --git a/Resources/doc/data-loader/gridfs.md b/Resources/doc/data-loader/gridfs.md
new file mode 100644
index 000000000..454c4478c
--- /dev/null
+++ b/Resources/doc/data-loader/gridfs.md
@@ -0,0 +1,32 @@
+# GridFSLoader
+
+Load your images from [MongoDB GridFS](http://docs.mongodb.org/manual/applications/gridfs/).
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_special_style:
+ data_loader: grid_fs
+ filters:
+ my_custom_filter: { }
+```
+
+Add loader to your services:
+
+``` xml
+
+
+
+
+ Application\ImageBundle\Document\Image
+
+```
+
+Reference the image by its id:
+
+``` jinja
+
+```
+
+- [Back to data loaders](../data-loaders.md)
+- [Back to the index](../index.md)
diff --git a/Resources/doc/data-loader/stream.md b/Resources/doc/data-loader/stream.md
new file mode 100644
index 000000000..e190c4bfc
--- /dev/null
+++ b/Resources/doc/data-loader/stream.md
@@ -0,0 +1,25 @@
+# StreamLoader
+
+The `Liip\ImagineBundle\Imagine\Data\Loader\StreamLoader` allows to read images from any stream registered
+thus allowing you to serve your images from literally anywhere.
+
+The example service definition shows how to use a stream wrapped by the [Gaufrette](https://github.com/KnpLabs/Gaufrette) filesystem abstraction layer.
+In order to have this example working, you need to register the stream wrapper first,
+refer to the [Gaufrette README](https://github.com/KnpLabs/Gaufrette/blob/master/README.markdown) on how to do this.
+
+If you are using the [KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle)
+you can make use of the [StreamWrapper configuration](https://github.com/KnpLabs/KnpGaufretteBundle#stream-wrapper) to register the filesystems.
+
+``` yaml
+services:
+ liip_imagine.data.loader.stream.profile_photos:
+ class: "%liip_imagine.data.loader.stream.class%"
+ arguments:
+ - "@liip_imagine"
+ - 'gaufrette://profile_photos/'
+ tags:
+ - { name: 'liip_imagine.data.loader', loader: 'stream.profile_photos' }
+```
+
+- [Back to data loaders](../data-loaders.md)
+- [Back to the index](../index.md)
diff --git a/Resources/doc/data-loaders.md b/Resources/doc/data-loaders.md
new file mode 100644
index 000000000..e7507e8ee
--- /dev/null
+++ b/Resources/doc/data-loaders.md
@@ -0,0 +1,92 @@
+# Built-In DataLoader
+
+* [MongoDB GridFS](data-loader/gridfs.md)
+* [Stream](data-loader/stream.md)
+
+# Other data loaders
+
+* [Doctrine PHPCR-ODM](http://symfony.com/doc/master/cmf/bundles/media.html#liipimagine)
+ You can include the CmfMediaBundle alone if you just want to use the images
+ but no other CMF features.
+
+# Custom image loaders
+
+The ImagineBundle allows you to add your custom image loader classes. The only
+requirement is that each data loader implement the following interface:
+
+ Liip\ImagineBundle\Imagine\Data\Loader\LoaderInterface
+
+To tell the bundle about your new data loader, register it in the service
+container and apply the `liip_imagine.data.loader` tag to it (example here in XML):
+
+``` xml
+
+
+
+ %liip_imagine.formats%
+
+```
+
+For more information on the service container, see the Symfony2
+[Service Container](http://symfony.com/doc/current/book/service_container.html) documentation.
+
+You can set your custom data loader by adding it to the your configuration as the new
+default loader as follows:
+
+``` yaml
+liip_imagine:
+ data_loader: my_custom_data
+```
+
+Alternatively you can only set the custom data loader for just a specific filter set:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_special_style:
+ data_loader: my_custom_data
+ filters:
+ my_custom_filter: { }
+```
+
+
+For an example of a data loader implementation, refer to
+`Liip\ImagineBundle\Imagine\Data\Loader\FileSystemLoader`.
+
+## Extending the image loader with data transformers
+
+You can extend a custom data loader to support virtually any file type using transformers.
+A data tranformer is intended to transform a file before actually rendering it. You
+can refer to `Liip\ImagineBundle\Imagine\Data\Loader\ExtendedFileSystemLoader` and
+to `Liip\ImagineBundle\Imagine\Data\Transformer\PdfTransformer` as an example.
+
+ExtendedFileSystemLoader extends FileSystemLoader and takes, as argument, an array of transformers.
+In the example, when a file with the pdf extension is passed to the data loader,
+PdfTransformer uses a php imagick object (injected via the service container)
+to extract the first page of the document and returns it to the data loader as a png image.
+
+To tell the bundle about the transformers, you have to register them as services
+with the new loader:
+
+```yml
+services:
+ imagick_object:
+ class: Imagick
+ acme_custom_transformer:
+ class: Acme\ImagineBundle\Imagine\Data\Transformer\MyCustomTransformer
+ arguments:
+ - '@imagick_object'
+ custom_loader:
+ class: Acme\ImagineBundle\Imagine\Data\Loader\MyCustomDataLoader
+ tags:
+ - { name: liip_imagine.data.loader, loader: custom_data_loader }
+ arguments:
+ - '@liip_imagine'
+ - %liip_imagine.formats%
+ - %liip_imagine.data_root%
+ - [ '@acme_custom_transformer' ]
+```
+
+Now you can use your custom data loader, with its transformers, setting it as in the previous section.
+
+[Back to the index](index.md)
diff --git a/Resources/doc/filters.md b/Resources/doc/filters.md
new file mode 100644
index 000000000..e0e81e58c
--- /dev/null
+++ b/Resources/doc/filters.md
@@ -0,0 +1,196 @@
+# Filters
+
+## Built-in Filters
+
+### The `thumbnail` filter
+
+The thumbnail filter, as the name implies, performs a thumbnail transformation
+on your image.
+
+The `mode` can be either `outbound` or `inset`.
+Option `inset` does a relative resize, where the height and the width will not exceed the values in the configuration.
+Option `outbound` does a relative resize, but the image gets cropped if width and height are not the same.
+
+Given an input image sized 50x40 (width x height), consider the following
+annotated configuration examples:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_thumb_out:
+ filters:
+ thumbnail: { size: [32, 32], mode: outbound } # Transforms 50x40 to 32x32, while cropping the width
+ my_thumb_in:
+ filters:
+ thumbnail: { size: [32, 32], mode: inset } # Transforms 50x40 to 32x26, no cropping
+```
+
+There is also an option `allow_upscale` (default: `false`).
+By setting `allow_upscale` to `true`, an image which is smaller than 32x32px in the example above will be expanded to the requested size by interpolation of its content.
+Without this option, a smaller image will be left as it. This means you may get images that are smaller than the specified dimensions.
+
+### The `relative_resize` filter
+
+The `relative_resize` filter may be used to `heighten`, `widen`, `increase` or
+`scale` an image with respect to its existing dimensions. These options directly
+correspond to methods on Imagine's `BoxInterface`.
+
+Given an input image sized 50x40 (width, height), consider the following
+annotated configuration examples:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_heighten:
+ filters:
+ relative_resize: { heighten: 60 } # Transforms 50x40 to 75x60
+ my_widen:
+ filters:
+ relative_resize: { widen: 32 } # Transforms 50x40 to 32x26
+ my_increase:
+ filters:
+ relative_resize: { increase: 10 } # Transforms 50x40 to 60x50
+ my_widen:
+ filters:
+ relative_resize: { scale: 2.5 } # Transforms 50x40 to 125x100
+```
+
+### The `upscale` filter
+
+The upscale filter, as the name implies, performs a upscale transformation
+on your image. Configuration looks like this:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_thumb:
+ filters:
+ upscale: { min: [800, 600] }
+```
+
+### The `crop` filter
+
+The crop filter, as the name implies, performs a crop transformation
+on your image. Configuration looks like this:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_thumb:
+ filters:
+ crop: { start: [10, 20], size: [120, 90] }
+```
+
+### The `strip` filter
+
+The strip filter removes all profiles and comments from your image.
+Configuration looks like this:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_thumb:
+ filters:
+ strip: ~
+```
+
+### The `background` filter
+
+The background filter sets a background color for your image, default is white (#FFF).
+Configuration looks like this:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_thumb:
+ filters:
+ background: { color: '#00FFFF' }
+```
+
+### The `watermark` filter
+
+The watermark filter pastes a second image onto your image while keeping its ratio.
+Configuration looks like this:
+
+``` yaml
+liip_image:
+ filter_sets:
+ my_image:
+ watermark:
+ image: Resources/data/watermark.png
+ # Size of the watermark relative to the origin images size
+ size: 0.5
+ # Position: One of topleft,top,topright,left,center,right,bottomleft,bottom,bottomright
+ position: center
+```
+
+## Load your Custom Filters
+
+The ImagineBundle allows you to load your own custom filter classes. The only
+requirement is that each filter loader implement the following interface:
+
+ Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface
+
+To tell the bundle about your new filter loader, register it in the service
+container and apply the `liip_imagine.filter.loader` tag to it (example here in XML):
+
+``` xml
+
+
+
+```
+
+For more information on the service container, see the Symfony2
+[Service Container](http://symfony.com/doc/current/book/service_container.html) documentation.
+
+You can now reference and use your custom filter when defining filter sets you'd
+like to apply in your configuration:
+
+``` yaml
+liip_imagine:
+ filter_sets:
+ my_special_style:
+ filters:
+ my_custom_filter: { }
+```
+
+For an example of a filter loader implementation, refer to
+`Liip\ImagineBundle\Imagine\Filter\Loader\ThumbnailFilterLoader`.
+
+## Dynamic filters
+
+With a custom data loader it is possible to dynamically modify the configuration that will
+be applied to the image. Inside the controller you can access the ``FilterConfiguration``
+instance, dynamically adjust the filter configuration (for example based on information
+associated with the image or whatever other logic you might want) and set it again.
+
+A simple example showing how to change the filter configuration dynamically. This example
+is of course "bogus" since hardcoded values could just as well be set in the configuration
+but it illustrates the core idea.
+
+``` php
+public function filterAction(Request $request, $path, $filter)
+{
+ $targetPath = $this->cacheManager->resolve($request, $path, $filter);
+ if ($targetPath instanceof Response) {
+ return $targetPath;
+ }
+
+ $image = $this->dataManager->find($filter, $path);
+
+ $filterConfig = $this->filterManager->getFilterConfiguration();
+ $config = $filterConfig->get($filter);
+ $config['filters']['thumbnail']['size'] = array(300, 100);
+ $filterConfig->set($filter, $config);
+
+ $response = $this->filterManager->get($request, $filter, $image, $path);
+
+ if ($targetPath) {
+ $response = $this->cacheManager->store($response, $targetPath, $filter);
+ }
+
+ return $response;
+}
+```
+
+[Back to the index](index.md)
diff --git a/Resources/doc/index.md b/Resources/doc/index.md
new file mode 100644
index 000000000..3705c0592
--- /dev/null
+++ b/Resources/doc/index.md
@@ -0,0 +1,8 @@
+# LiipImagineBundle
+
+* [Installation](installation.md)
+* [Introduction](introduction.md)
+* [Configuration](configuration.md)
+* [Filters](filters.md)
+* [DataLoaders](data-loaders.md)
+* [CacheResolvers](cache-resolvers.md)
diff --git a/Resources/doc/installation.md b/Resources/doc/installation.md
new file mode 100644
index 000000000..83b644ab8
--- /dev/null
+++ b/Resources/doc/installation.md
@@ -0,0 +1,49 @@
+# Installation
+
+To install this bundle, you'll need both the [Imagine library](https://github.com/avalanche123/Imagine)
+and this bundle.
+
+### Step 1: Download LiipImagineBundle using composer
+
+Tell composer to require LiipImagineBundle by running the command:
+
+``` bash
+$ php composer.phar require "liip/imagine-bundle:dev-master"
+```
+
+Composer will install the bundle to your project's `vendor/liip/imagine-bundle` directory.
+
+
+## Step 2: Enable the bundle
+
+Finally, enable the bundle in the kernel:
+
+``` php
+
+ {% if link_url %}
+
+ {% endif %}
+
+
+
+ {% if link_url %}
+
+ {% endif %}
+
+ {% endif %}
+
+ {{ block('form_widget_simple') }}
+ {% endspaceless %}
+{% endblock %}
diff --git a/Routing/ImagineLoader.php b/Routing/ImagineLoader.php
index bf8414f1b..c26a673c3 100644
--- a/Routing/ImagineLoader.php
+++ b/Routing/ImagineLoader.php
@@ -2,20 +2,21 @@
namespace Liip\ImagineBundle\Routing;
+use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\Route;
-
use Symfony\Component\Routing\RouteCollection;
-use Symfony\Component\Config\Loader\Loader;
class ImagineLoader extends Loader
{
+ private $controllerAction;
private $cachePrefix;
private $filters;
- public function __construct($cachePrefix, array $filters = array())
+ public function __construct($controllerAction, $cachePrefix, array $filters = array())
{
+ $this->controllerAction = $controllerAction;
$this->cachePrefix = $cachePrefix;
- $this->filters = $filters;
+ $this->filters = $filters;
}
public function supports($resource, $type = null)
@@ -26,7 +27,6 @@ public function supports($resource, $type = null)
public function load($resource, $type = null)
{
$requirements = array('_method' => 'GET', 'filter' => '[A-z0-9_\-]*', 'path' => '.+');
- $defaults = array('_controller' => 'liip_imagine.controller:filterAction');
$routes = new RouteCollection();
if (count($this->filters) > 0) {
@@ -40,10 +40,30 @@ public function load($resource, $type = null)
$pattern .= '/'.$filter;
}
+ $defaults = array(
+ '_controller' => empty($config['controller_action']) ? $this->controllerAction : $config['controller_action'],
+ 'filter' => $filter,
+ );
+
+ $routeRequirements = $requirements;
+ $routeDefaults = $defaults;
+ $routeOptions = array();
+
+ if (isset($config['route']['requirements'])) {
+ $routeRequirements = array_merge($routeRequirements, $config['route']['requirements']);
+ }
+ if (isset($config['route']['defaults'])) {
+ $routeDefaults = array_merge($routeDefaults, $config['route']['defaults']);
+ }
+ if (isset($config['route']['options'])) {
+ $routeOptions = array_merge($routeOptions, $config['route']['options']);
+ }
+
$routes->add('_imagine_'.$filter, new Route(
$pattern.'/{path}',
- array_merge( $defaults, array('filter' => $filter)),
- $requirements
+ $routeDefaults,
+ $routeRequirements,
+ $routeOptions
));
}
}
diff --git a/Templating/Helper/ImagineHelper.php b/Templating/Helper/ImagineHelper.php
index 8d5a52a24..a98260db7 100644
--- a/Templating/Helper/ImagineHelper.php
+++ b/Templating/Helper/ImagineHelper.php
@@ -2,28 +2,28 @@
namespace Liip\ImagineBundle\Templating\Helper;
-use Liip\ImagineBundle\Imagine\CachePathResolver;
+use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Symfony\Component\Templating\Helper\Helper;
class ImagineHelper extends Helper
{
/**
- * @var Liip\ImagineBundle\Imagine\CachePathResolver
+ * @var CacheManager
*/
- private $cachePathResolver;
+ protected $cacheManager;
/**
- * Constructs by setting $cachePathResolver
+ * Constructor.
*
- * @param Liip\ImagineBundle\Imagine\CachePathResolver $cachePathResolver
+ * @param CacheManager $cacheManager
*/
- public function __construct(CachePathResolver $cachePathResolver)
+ public function __construct(CacheManager $cacheManager)
{
- $this->cachePathResolver = $cachePathResolver;
+ $this->cacheManager = $cacheManager;
}
/**
- * Gets cache path of an image to be filtered
+ * Gets the browser path for the image and filter to apply.
*
* @param string $path
* @param string $filter
@@ -33,15 +33,14 @@ public function __construct(CachePathResolver $cachePathResolver)
*/
public function filter($path, $filter, $absolute = false)
{
- return $this->cachePathResolver->getBrowserPath($path, $filter, $absolute);
+ return $this->cacheManager->getBrowserPath($path, $filter, $absolute);
}
/**
- * (non-PHPdoc)
- * @see Symfony\Component\Templating\Helper.HelperInterface::getName()
+ * {@inheritDoc}
*/
public function getName()
{
- return 'imagine';
+ return 'liip_imagine';
}
}
diff --git a/Templating/ImagineExtension.php b/Templating/ImagineExtension.php
index 4d6fc44f7..ef8aab02d 100644
--- a/Templating/ImagineExtension.php
+++ b/Templating/ImagineExtension.php
@@ -2,29 +2,27 @@
namespace Liip\ImagineBundle\Templating;
-use Liip\ImagineBundle\Imagine\CachePathResolver;
-use Symfony\Component\HttpKernel\Util\Filesystem;
+use Liip\ImagineBundle\Imagine\Cache\CacheManager;
class ImagineExtension extends \Twig_Extension
{
/**
- * @var Liip\ImagineBundle\Imagine\CachePathResolver
+ * @var CacheManager
*/
- private $cachePathResolver;
+ private $cacheManager;
/**
- * Constructs by setting $cachePathResolver
+ * Constructor.
*
- * @param Liip\ImagineBundle\Imagine\CachePathResolver $cachePathResolver
+ * @param CacheManager $cacheManager
*/
- public function __construct(CachePathResolver $cachePathResolver)
+ public function __construct(CacheManager $cacheManager)
{
- $this->cachePathResolver = $cachePathResolver;
+ $this->cacheManager = $cacheManager;
}
/**
- * (non-PHPdoc)
- * @see Twig_Extension::getFilters()
+ * {@inheritDoc}
*/
public function getFilters()
{
@@ -34,7 +32,7 @@ public function getFilters()
}
/**
- * Gets cache path of an image to be filtered
+ * Gets the browser path for the image and filter to apply.
*
* @param string $path
* @param string $filter
@@ -44,15 +42,14 @@ public function getFilters()
*/
public function filter($path, $filter, $absolute = false)
{
- return $this->cachePathResolver->getBrowserPath($path, $filter, $absolute);
+ return $this->cacheManager->getBrowserPath($path, $filter, $absolute);
}
/**
- * (non-PHPdoc)
- * @see Twig_ExtensionInterface::getName()
+ * {@inheritDoc}
*/
public function getName()
{
- return 'imagine';
+ return 'liip_imagine';
}
}
diff --git a/Tests/AbstractTest.php b/Tests/AbstractTest.php
new file mode 100644
index 000000000..13a0a53d3
--- /dev/null
+++ b/Tests/AbstractTest.php
@@ -0,0 +1,93 @@
+fixturesDir = __DIR__.'/Fixtures';
+
+ $this->tempDir = str_replace('/', DIRECTORY_SEPARATOR, sys_get_temp_dir().'/liip_imagine_test');
+
+ $this->filesystem = new Filesystem();
+
+ if ($this->filesystem->exists($this->tempDir)) {
+ $this->filesystem->remove($this->tempDir);
+ }
+
+ $this->filesystem->mkdir($this->tempDir);
+ }
+
+ public function invalidPathProvider()
+ {
+ return array(
+ array($this->fixturesDir.'/assets/../../foobar.png'),
+ array($this->fixturesDir.'/assets/some_folder/../foobar.png'),
+ array('../../outside/foobar.jpg'),
+ );
+ }
+
+ protected function createFilterConfiguration()
+ {
+ $config = new FilterConfiguration();
+ $config->set('thumbnail', array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ ));
+
+ return $config;
+ }
+
+ protected function getMockCacheManager()
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Cache\CacheManager', array(), array(), '', false);
+ }
+
+ protected function getMockFilterConfiguration()
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Filter\FilterConfiguration');
+ }
+
+ protected function getMockRouter()
+ {
+ return $this->getMock('Symfony\Component\Routing\RouterInterface');
+ }
+
+ protected function getMockResolver()
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Cache\Resolver\ResolverInterface');
+ }
+
+ protected function getMockImage()
+ {
+ return $this->getMock('Imagine\Image\ImageInterface');
+ }
+
+ protected function getMockImagine()
+ {
+ return $this->getMock('Imagine\Image\ImagineInterface');
+ }
+
+ protected function tearDown()
+ {
+ if (!$this->filesystem) {
+ return;
+ }
+
+ if ($this->filesystem->exists($this->tempDir)) {
+ $this->filesystem->remove($this->tempDir);
+ }
+ }
+}
diff --git a/Tests/Controller/ImagineControllerTest.php b/Tests/Controller/ImagineControllerTest.php
new file mode 100644
index 000000000..891df2c25
--- /dev/null
+++ b/Tests/Controller/ImagineControllerTest.php
@@ -0,0 +1,132 @@
+imagine = new $eachClass;
+
+ break;
+ } catch (\Exception $e) { }
+ }
+
+ if (!$this->imagine) {
+ $this->markTestSkipped('No Imagine could be instantiated.');
+ }
+
+ $this->webRoot = $this->tempDir.'/web';
+ $this->filesystem->mkdir($this->webRoot);
+
+ $this->cacheDir = $this->webRoot.'/media/cache';
+ $this->dataDir = $this->fixturesDir.'/assets';
+
+ $this->configuration = new FilterConfiguration(array(
+ 'thumbnail' => array(
+ 'filters' => array(
+ 'thumbnail' => array(
+ 'size' => array(300, 150),
+ 'mode' => 'outbound',
+ ),
+ ),
+ ),
+ ));
+ }
+
+ public function testFilterActionLive()
+ {
+ $router = $this->getMockRouter();
+ $router
+ ->expects($this->any())
+ ->method('generate')
+ ->with('_imagine_thumbnail', array(
+ 'path' => 'cats.jpeg'
+ ), false)
+ ->will($this->returnValue('/media/cache/thumbnail/cats.jpeg'))
+ ;
+
+ $dataLoader = new FileSystemLoader($this->imagine, array(), $this->dataDir);
+
+ $dataManager = new DataManager($this->configuration, 'filesystem');
+ $dataManager->addLoader('filesystem', $dataLoader);
+
+ $filterLoader = new ThumbnailFilterLoader();
+
+ $filterManager = new FilterManager($this->configuration);
+ $filterManager->addLoader('thumbnail', $filterLoader);
+
+ $webPathResolver = new WebPathResolver($this->filesystem);
+
+ $cacheManager = new CacheManager($this->configuration, $router, $this->webRoot, 'web_path');
+ $cacheManager->addResolver('web_path', $webPathResolver);
+
+ $controller = new ImagineController($dataManager, $filterManager, $cacheManager);
+
+ $request = Request::create('/media/cache/thumbnail/cats.jpeg');
+ $response = $controller->filterAction($request, 'cats.jpeg', 'thumbnail');
+
+ $targetPath = realpath($this->webRoot).'/media/cache/thumbnail/cats.jpeg';
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(201, $response->getStatusCode());
+ $this->assertTrue(file_exists($targetPath));
+ $this->assertNotEmpty(file_get_contents($targetPath));
+
+ return $controller;
+ }
+
+ public function testFilterDelegatesResolverResponse()
+ {
+ $response = $this->getMock('Symfony\Component\HttpFoundation\Response');
+
+ $cacheManager = $this->getMockCacheManager();
+ $cacheManager
+ ->expects($this->once())
+ ->method('resolve')
+ ->will($this->returnValue($response))
+ ;
+
+ $dataManager = $this->getMock('Liip\ImagineBundle\Imagine\Data\DataManager', array(), array($this->configuration));
+ $filterManager = $this->getMock('Liip\ImagineBundle\Imagine\Filter\FilterManager', array(), array($this->configuration));
+
+ $controller = new ImagineController($dataManager, $filterManager, $cacheManager);
+ $this->assertSame($response, $controller->filterAction(Request::create('/media/cache/thumbnail/cats.jpeg'), 'cats.jpeg', 'thumbnail'));
+ }
+}
diff --git a/Tests/DependencyInjection/LiipImagineExtensionTest.php b/Tests/DependencyInjection/LiipImagineExtensionTest.php
index 8354f2f17..5ad19a3b2 100644
--- a/Tests/DependencyInjection/LiipImagineExtensionTest.php
+++ b/Tests/DependencyInjection/LiipImagineExtensionTest.php
@@ -1,22 +1,20 @@
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
namespace Liip\ImagineBundle\Tests\DependencyInjection;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Liip\ImagineBundle\Tests\AbstractTest;
use Liip\ImagineBundle\DependencyInjection\LiipImagineExtension;
-use Symfony\Component\Yaml\Parser;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpKernel\Kernel;
+use Symfony\Component\Yaml\Parser;
-class LiipImagineExtensionTest extends \PHPUnit_Framework_TestCase
+/**
+ * @covers Liip\ImagineBundle\DependencyInjection\Configuration
+ * @covers Liip\ImagineBundle\DependencyInjection\LiipImagineExtension
+ */
+class LiipImagineExtensionTest extends AbstractTest
{
/**
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
@@ -37,26 +35,46 @@ public function testLoadWithDefaults()
{
$this->createEmptyConfiguration();
- $this->assertParameter(true, 'liip_imagine.cache');
+ $this->assertParameter('web_path', 'liip_imagine.cache.resolver.default');
$this->assertAlias('liip_imagine.gd', 'liip_imagine');
$this->assertHasDefinition('liip_imagine.controller');
$this->assertDICConstructorArguments(
$this->containerBuilder->getDefinition('liip_imagine.controller'),
- array(new Reference('liip_imagine.loader.filesystem'), new Reference('liip_imagine.filter.manager'), '%liip_imagine.web_root%', new Reference('liip_imagine.cache.path.resolver'))
+ array(new Reference('liip_imagine.data.manager'), new Reference('liip_imagine.filter.manager'), new Reference('liip_imagine.cache.manager'))
);
}
- public function testLoad()
+ public function testCacheClearerRegistration()
+ {
+ $this->createEmptyConfiguration();
+
+ if ('2' == Kernel::MAJOR_VERSION && '0' == Kernel::MINOR_VERSION) {
+ $this->assertFalse($this->containerBuilder->hasDefinition('liip_imagine.cache.clearer'));
+ } else {
+ $this->assertTrue($this->containerBuilder->hasDefinition('liip_imagine.cache.clearer'));
+
+ $definition = $this->containerBuilder->getDefinition('liip_imagine.cache.clearer');
+ $definition->hasTag('kernel.cache_clearer');
+ $this->assertCount(2, $definition->getArguments());
+ }
+ }
+
+ public function testCacheClearerIsNotRegistered()
{
$this->createFullConfiguration();
- $this->assertParameter(false, 'liip_imagine.cache');
- $this->assertAlias('liip_imagine.imagick', 'liip_imagine');
- $this->assertHasDefinition('liip_imagine.controller');
- $this->assertDICConstructorArguments(
- $this->containerBuilder->getDefinition('liip_imagine.controller'),
- array(new Reference('acme_liip_imagine.loader'), new Reference('liip_imagine.filter.manager'), '%liip_imagine.web_root%')
- );
+ $this->assertFalse($this->containerBuilder->hasDefinition('liip_imagine.cache.clearer'));
+ }
+
+ public function testCustomRouteRequirements()
+ {
+ $this->createFullConfiguration();
+ $param = $this->containerBuilder->getParameter('liip_imagine.filter_sets');
+
+ $this->assertTrue(isset($param['small']['filters']['route']['requirements']));
+
+ $variable1 = $param['small']['filters']['route']['requirements']['variable1'];
+ $this->assertEquals('value1', $variable1, sprintf('%s parameter is correct', $variable1));
}
/**
@@ -88,11 +106,14 @@ protected function getFullConfig()
web_root: ../foo/bar
cache_prefix: /imagine/cache
cache: false
+cache_clearer: false
formats: ['json', 'xml', 'jpg', 'png', 'gif']
filter_sets:
small:
filters:
thumbnail: { size: [100, ~], mode: inset }
+ route:
+ requirements: { variable1: 'value1' }
quality: 80
medium_small_cropped:
filters:
@@ -115,7 +136,7 @@ protected function getFullConfig()
quality: 100
'':
quality: 100
-loader: acme_liip_imagine.loader
+data_loader: my_loader
EOF;
$parser = new Parser();
diff --git a/Tests/Fixtures/AmazonS3.php b/Tests/Fixtures/AmazonS3.php
new file mode 100644
index 000000000..809aa1510
--- /dev/null
+++ b/Tests/Fixtures/AmazonS3.php
@@ -0,0 +1,23 @@
+data[$id])) ? $this->data[$id] : false;
+ }
+
+ public function contains($id)
+ {
+ return isset($this->data[$id]);
+ }
+
+ public function save($id, $data, $lifeTime = 0)
+ {
+ $this->data[$id] = $data;
+
+ return true;
+ }
+
+ public function delete($id)
+ {
+ unset($this->data[$id]);
+
+ return true;
+ }
+
+ public function getStats()
+ {
+ return null;
+ }
+}
diff --git a/Tests/Fixtures/Model.php b/Tests/Fixtures/Model.php
new file mode 100644
index 000000000..0de5eac3b
--- /dev/null
+++ b/Tests/Fixtures/Model.php
@@ -0,0 +1,8 @@
+markTestSkipped('The CacheClearerInterface does not exist.');
+ }
+
+ parent::setUp();
+ }
+
+ public function testClearIgnoresCacheDirectory()
+ {
+ $cacheManager = $this->getMockCacheManager();
+ $cacheManager
+ ->expects($this->once())
+ ->method('clearResolversCache')
+ ->with('/media/cache')
+ ;
+
+ $cacheClearer = new CacheClearer($cacheManager, '/media/cache');
+ $cacheClearer->clear($this->tempDir.'/cache');
+ }
+}
diff --git a/Tests/Imagine/Cache/CacheManagerTest.php b/Tests/Imagine/Cache/CacheManagerTest.php
new file mode 100644
index 000000000..d7c61d4bb
--- /dev/null
+++ b/Tests/Imagine/Cache/CacheManagerTest.php
@@ -0,0 +1,220 @@
+getMockFilterConfiguration(), $this->getMockRouter(), $this->fixturesDir.'/assets');
+ $this->assertEquals(str_replace('/', DIRECTORY_SEPARATOR, $this->fixturesDir.'/assets'), $cacheManager->getWebRoot());
+ }
+
+ public function testAddCacheManagerAwareResolver()
+ {
+ $cacheManager = new CacheManager($this->getMockFilterConfiguration(), $this->getMockRouter(), $this->fixturesDir.'/assets');
+
+ $resolver = $this->getMock('Liip\ImagineBundle\Tests\Fixtures\CacheManagerAwareResolver');
+ $resolver
+ ->expects($this->once())
+ ->method('setCacheManager')
+ ->with($cacheManager)
+ ;
+
+ $cacheManager->addResolver('thumbnail', $resolver);
+ }
+
+ public function testGetBrowserPathWithoutResolver()
+ {
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->once())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'cache' => null,
+ )))
+ ;
+
+ $cacheManager = new CacheManager($config, $this->getMockRouter(), $this->fixturesDir.'/assets', 'default');
+
+ $this->setExpectedException('InvalidArgumentException', 'Could not find resolver for "thumbnail" filter type');
+ $cacheManager->getBrowserPath('cats.jpeg', 'thumbnail', true);
+ }
+
+ public function testDefaultResolverUsedIfNoneSet()
+ {
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->once())
+ ->method('getBrowserPath')
+ ->with('cats.jpeg', 'thumbnail', true)
+ ;
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->once())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'cache' => null,
+ )))
+ ;
+
+ $cacheManager = new CacheManager($config, $this->getMockRouter(), $this->fixturesDir.'/assets', 'default');
+ $cacheManager->addResolver('default', $resolver);
+
+ $cacheManager->getBrowserPath('cats.jpeg', 'thumbnail', true);
+ }
+
+ /**
+ * @dataProvider invalidPathProvider
+ */
+ public function testResolveInvalidPath($path)
+ {
+ $cacheManager = new CacheManager($this->getMockFilterConfiguration(), $this->getMockRouter(), $this->fixturesDir.'/assets');
+
+ $this->setExpectedException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException');
+ $cacheManager->resolve(new Request(), $path, 'thumbnail');
+ }
+
+ public function testResolveWithoutResolver()
+ {
+ $cacheManager = new CacheManager($this->getMockFilterConfiguration(), $this->getMockRouter(), $this->fixturesDir.'/assets');
+
+ $this->assertFalse($cacheManager->resolve(new Request(), 'cats.jpeg', 'thumbnail'));
+ }
+
+ public function testFallbackToDefaultResolver()
+ {
+ $response = new Response('', 200);
+ $request = new Request();
+
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->once())
+ ->method('resolve')
+ ->with($request, 'cats.jpeg', 'thumbnail')
+ ->will($this->returnValue('/thumbs/cats.jpeg'))
+ ;
+ $resolver
+ ->expects($this->once())
+ ->method('store')
+ ->with($response, '/thumbs/cats.jpeg', 'thumbnail')
+ ->will($this->returnValue($response))
+ ;
+ $resolver
+ ->expects($this->once())
+ ->method('remove')
+ ->with('/thumbs/cats.jpeg', 'thumbnail')
+ ->will($this->returnValue(true))
+ ;
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->exactly(3))
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'cache' => null,
+ )))
+ ;
+
+ $cacheManager = new CacheManager($config, $this->getMockRouter(), $this->fixturesDir.'/assets', 'default');
+ $cacheManager->addResolver('default', $resolver);
+
+ // Resolve fallback to default resolver
+ $this->assertEquals('/thumbs/cats.jpeg', $cacheManager->resolve($request, 'cats.jpeg', 'thumbnail'));
+
+ // Store fallback to default resolver
+ $this->assertEquals($response, $cacheManager->store($response, '/thumbs/cats.jpeg', 'thumbnail'));
+
+ // Remove fallback to default resolver
+ $this->assertTrue($cacheManager->remove('/thumbs/cats.jpeg', 'thumbnail'));
+ }
+
+ public function testClearResolversCacheClearsAll()
+ {
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->exactly(5))
+ ->method('clear')
+ ->with('imagine_cache')
+ ;
+
+ $cacheManager = new CacheManager($this->getMockFilterConfiguration(), $this->getMockRouter(), $this->fixturesDir.'/assets', 'default');
+
+ $cacheManager->addResolver('default', $resolver);
+ $cacheManager->addResolver('thumbnail1', $resolver);
+ $cacheManager->addResolver('thumbnail2', $resolver);
+ $cacheManager->addResolver('thumbnail3', $resolver);
+ $cacheManager->addResolver('thumbnail4', $resolver);
+
+ $cacheManager->clearResolversCache('imagine_cache');
+ }
+
+ public function generateUrlProvider()
+ {
+ return array(
+ // Simple route generation
+ array(array(), '/thumbnail/cats.jpeg', 'thumbnail/cats.jpeg'),
+
+ // 'format' given, altering URL
+ array(array(
+ 'format' => 'jpg',
+ ), '/thumbnail/cats.jpeg', 'thumbnail/cats.jpg'),
+
+ // No 'extension' path info
+ array(array(
+ 'format' => 'jpg',
+ ), '/thumbnail/cats', 'thumbnail/cats.jpg'),
+
+ // No 'extension' path info, and no directory
+ array(array(
+ 'format' => 'jpg',
+ ), '/cats', 'cats.jpg'),
+ );
+ }
+
+ /**
+ * @dataProvider generateUrlProvider
+ */
+ public function testGenerateUrl($filterConfig, $targetPath, $expectedPath)
+ {
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->once())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue($filterConfig))
+ ;
+
+ $router = $this->getMockRouter();
+ $router
+ ->expects($this->once())
+ ->method('generate')
+ ->with('_imagine_thumbnail', array(
+ 'path' => $expectedPath,
+ ), true)
+ ;
+
+ $cacheManager = new CacheManager($config, $router, $this->fixturesDir.'/assets');
+ $cacheManager->generateUrl($targetPath, 'thumbnail', true);
+ }
+}
diff --git a/Tests/Imagine/Cache/Resolver/AbstractFilesystemResolverTest.php b/Tests/Imagine/Cache/Resolver/AbstractFilesystemResolverTest.php
new file mode 100644
index 000000000..4c5adcc88
--- /dev/null
+++ b/Tests/Imagine/Cache/Resolver/AbstractFilesystemResolverTest.php
@@ -0,0 +1,78 @@
+markTestSkipped('file_get_contents can not read files with utf-8 file names on windows');
+ }
+
+ $image = $this->fixturesDir.'/assets/АГГЗ.jpeg';
+
+ $data = file_get_contents($image);
+
+ $response = new Response($data, 200, array(
+ 'content-type' => 'image/jpeg',
+ ));
+
+ $targetPath = $this->tempDir.'/cached/АГГЗ.jpeg';
+
+ $resolver = $this->getMockAbstractFilesystemResolver(new Filesystem());
+ $resolver->store($response, $targetPath, 'mirror');
+
+ $this->assertTrue(file_exists($targetPath));
+ $this->assertEquals($data, file_get_contents($targetPath));
+ }
+
+ public function testMkdirVerifyPermissionOnLastLevel () {
+ if (false !== strpos(strtolower(PHP_OS), 'win')) {
+ $this->markTestSkipped('mkdir mode is ignored on windows');
+ }
+
+ $resolver = $this->getMockAbstractFilesystemResolver(new Filesystem());
+
+
+ $resolver->store(new Response(''), $this->tempDir . '/first-level/second-level/cats.jpeg', 'thumbnail');
+ $this->assertEquals(040777, fileperms($this->tempDir . '/first-level/second-level'));
+ }
+
+ public function testMkdirVerifyPermissionOnFirstLevel () {
+ if (false !== strpos(strtolower(PHP_OS), 'win')) {
+ $this->markTestSkipped('mkdir mode is ignored on windows');
+ }
+
+ $resolver = $this->getMockAbstractFilesystemResolver(new Filesystem());
+
+ $resolver->store(new Response(''), $this->tempDir . '/first-level/second-level/cats.jpeg', 'thumbnail');
+ $this->assertEquals(040777, fileperms($this->tempDir . '/first-level'));
+ }
+
+ public function testStoreInvalidDirectory()
+ {
+ if (false !== strpos(strtolower(PHP_OS), 'win')) {
+ $this->markTestSkipped('mkdir mode is ignored on windows');
+ }
+
+ $resolver = $this->getMockAbstractFilesystemResolver(new Filesystem());
+
+ $this->filesystem->mkdir($this->tempDir.'/unwriteable', 0555);
+
+ $this->setExpectedException('RuntimeException', 'Could not create directory '.dirname($this->tempDir.'/unwriteable/thumbnail/cats.jpeg'));
+ $resolver->store(new Response(''), $this->tempDir.'/unwriteable/thumbnail/cats.jpeg', 'thumbnail');
+ }
+
+ protected function getMockAbstractFilesystemResolver($filesystem)
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Cache\Resolver\AbstractFilesystemResolver', array('resolve', 'clear', 'getBrowserPath', 'getFilePath'), array($filesystem));
+ }
+}
diff --git a/Tests/Imagine/Cache/Resolver/AmazonS3ResolverTest.php b/Tests/Imagine/Cache/Resolver/AmazonS3ResolverTest.php
new file mode 100644
index 000000000..d81071e61
--- /dev/null
+++ b/Tests/Imagine/Cache/Resolver/AmazonS3ResolverTest.php
@@ -0,0 +1,241 @@
+fixturesDir.'/AmazonS3.php');
+ }
+ }
+
+ public function testNoDoubleSlashesInObjectUrl()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('get_object_url')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb');
+ }
+
+ public function testObjUrlOptions()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('get_object_url')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg', 0, array('torrent' => true))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $resolver->setObjectUrlOption('torrent', true);
+ $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb');
+ }
+
+ public function testBrowserPathNotExisting()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(false))
+ ;
+ $s3
+ ->expects($this->never())
+ ->method('get_object_url')
+ ;
+
+ $cacheManager = $this->getMockCacheManager();
+ $cacheManager
+ ->expects($this->once())
+ ->method('generateUrl')
+ ->with('/some-folder/targetpath.jpg', 'thumb', false)
+ ->will($this->returnValue('/media/cache/thumb/some-folder/targetpath.jpg'))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $resolver->setCacheManager($cacheManager);
+
+ $this->assertEquals('/media/cache/thumb/some-folder/targetpath.jpg', $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testLogNotCreatedObjects()
+ {
+ $response = new Response();
+ $response->setContent('foo');
+ $response->headers->set('Content-Type', 'image/jpeg');
+
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('create_object')
+ ->will($this->returnValue($this->getS3ResponseMock(false)))
+ ;
+
+ $logger = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\Log\LoggerInterface');
+ $logger
+ ->expects($this->once())
+ ->method('warn')
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $resolver->setLogger($logger);
+
+ $this->assertSame($response, $resolver->store($response, 'foobar.jpg', 'thumb'));
+ }
+
+ public function testCreatedObjectRedirects()
+ {
+ $response = new Response();
+ $response->setContent('foo');
+ $response->headers->set('Content-Type', 'image/jpeg');
+
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('create_object')
+ ->will($this->returnValue($this->getS3ResponseMock(true)))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('get_object_url')
+ ->with('images.example.com', 'thumb/foobar.jpg', 0, array())
+ ->will($this->returnValue('http://images.example.com/thumb/foobar.jpg'))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+
+ $this->assertSame($response, $resolver->store($response, 'thumb/foobar.jpg', 'thumb'));
+ $this->assertEquals(301, $response->getStatusCode());
+ $this->assertEquals('http://images.example.com/thumb/foobar.jpg', $response->headers->get('Location'));
+ }
+
+ public function testResolveNewObject()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->will($this->returnValue(false))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $targetPath = $resolver->resolve(new Request(), '/some-folder/targetpath.jpg', 'thumb');
+
+ $this->assertEquals('thumb/some-folder/targetpath.jpg', $targetPath);
+ }
+
+ public function testResolveRedirectsOnExisting()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('get_object_url')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg', 0, array())
+ ->will($this->returnValue('http://images.example.com/some-folder/targetpath.jpg'))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $response = $resolver->resolve(new Request(), '/some-folder/targetpath.jpg', 'thumb');
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(301, $response->getStatusCode());
+ $this->assertEquals('http://images.example.com/some-folder/targetpath.jpg', $response->headers->get('Location'));
+ }
+
+ public function testRemove()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('delete_object')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue($this->getS3ResponseMock(true)))
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $this->assertTrue($resolver->remove('thumb/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testRemoveNotExisting()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->once())
+ ->method('if_object_exists')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(false))
+ ;
+ $s3
+ ->expects($this->never())
+ ->method('delete_object')
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $this->assertTrue($resolver->remove('thumb/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testClearIsDisabled()
+ {
+ $s3 = $this->getMock('AmazonS3');
+ $s3
+ ->expects($this->never())
+ ->method('delete_object')
+ ;
+
+ $resolver = new AmazonS3Resolver($s3, 'images.example.com');
+ $resolver->clear('');
+ }
+
+ protected function getS3ResponseMock($ok = true)
+ {
+ $s3Response = $this->getMock('AmazonS3Response');
+ $s3Response
+ ->expects($this->once())
+ ->method('isOK')
+ ->will($this->returnValue($ok))
+ ;
+
+ return $s3Response;
+ }
+}
diff --git a/Tests/Imagine/Cache/Resolver/AwsS3ResolverTest.php b/Tests/Imagine/Cache/Resolver/AwsS3ResolverTest.php
new file mode 100644
index 000000000..4c70c1db7
--- /dev/null
+++ b/Tests/Imagine/Cache/Resolver/AwsS3ResolverTest.php
@@ -0,0 +1,250 @@
+fixturesDir.'/S3Client.php');
+ }
+
+ if (!class_exists('Aws\S3\Enum\CannedAcl')) {
+ require_once($this->fixturesDir.'/CannedAcl.php');
+ }
+
+ if (!class_exists('Guzzle\Service\Resource\Model')) {
+ require_once($this->fixturesDir.'/Model.php');
+ }
+ }
+
+ public function testNoDoubleSlashesInObjectUrl()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('getObjectUrl')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb');
+ }
+
+ public function testObjUrlOptions()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('getObjectUrl')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg', 0, array('torrent' => true))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $resolver->setObjectUrlOption('torrent', true);
+ $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb');
+ }
+
+ public function testBrowserPathNotExisting()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(false))
+ ;
+ $s3
+ ->expects($this->never())
+ ->method('getObjectUrl')
+ ;
+
+ $cacheManager = $this->getMockCacheManager();
+ $cacheManager
+ ->expects($this->once())
+ ->method('generateUrl')
+ ->with('/some-folder/targetpath.jpg', 'thumb', false)
+ ->will($this->returnValue('/media/cache/thumb/some-folder/targetpath.jpg'))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $resolver->setCacheManager($cacheManager);
+
+ $this->assertEquals('/media/cache/thumb/some-folder/targetpath.jpg', $resolver->getBrowserPath('/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testLogNotCreatedObjects()
+ {
+ $response = new Response();
+ $response->setContent('foo');
+ $response->headers->set('Content-Type', 'image/jpeg');
+
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('putObject')
+ ->will($this->throwException(new \Exception))
+ ;
+
+ $logger = $this->getMockForAbstractClass('Symfony\Component\HttpKernel\Log\LoggerInterface');
+ $logger
+ ->expects($this->once())
+ ->method('warn')
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $resolver->setLogger($logger);
+
+ $this->assertSame($response, $resolver->store($response, 'foobar.jpg', 'thumb'));
+ }
+
+ public function testCreatedObjectRedirects()
+ {
+ $response = new Response();
+ $response->setContent('foo');
+ $response->headers->set('Content-Type', 'image/jpeg');
+
+ $responseMock = $this->getS3ResponseMock();
+
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('putObject')
+ ->will($this->returnValue($responseMock))
+ ;
+
+ $responseMock
+ ->expects($this->once())
+ ->method('get')
+ ->with('ObjectURL')
+ ->will($this->returnValue('http://images.example.com/thumb/foobar.jpg'))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+
+ $this->assertSame($response, $resolver->store($response, 'thumb/foobar.jpg', 'thumb'));
+ $this->assertEquals(301, $response->getStatusCode());
+ $this->assertEquals('http://images.example.com/thumb/foobar.jpg', $response->headers->get('Location'));
+ }
+
+ public function testResolveNewObject()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->will($this->returnValue(false))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $targetPath = $resolver->resolve(new Request(), '/some-folder/targetpath.jpg', 'thumb');
+
+ $this->assertEquals('thumb/some-folder/targetpath.jpg', $targetPath);
+ }
+
+ public function testResolveRedirectsOnExisting()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('getObjectUrl')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg', 0, array())
+ ->will($this->returnValue('http://images.example.com/some-folder/targetpath.jpg'))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $response = $resolver->resolve(new Request(), '/some-folder/targetpath.jpg', 'thumb');
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(301, $response->getStatusCode());
+ $this->assertEquals('http://images.example.com/some-folder/targetpath.jpg', $response->headers->get('Location'));
+ }
+
+ public function testRemove()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(true))
+ ;
+ $s3
+ ->expects($this->once())
+ ->method('deleteObject')
+ ->with(array(
+ 'Bucket' => 'images.example.com',
+ 'Key' => 'thumb/some-folder/targetpath.jpg',
+ ))
+ ->will($this->returnValue($this->getS3ResponseMock(true)))
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $this->assertTrue($resolver->remove('thumb/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testRemoveNotExisting()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->once())
+ ->method('doesObjectExist')
+ ->with('images.example.com', 'thumb/some-folder/targetpath.jpg')
+ ->will($this->returnValue(false))
+ ;
+ $s3
+ ->expects($this->never())
+ ->method('deleteObject')
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $this->assertTrue($resolver->remove('thumb/some-folder/targetpath.jpg', 'thumb'));
+ }
+
+ public function testClearIsDisabled()
+ {
+ $s3 = $this->getMock('Aws\S3\S3Client');
+ $s3
+ ->expects($this->never())
+ ->method('deleteObject')
+ ;
+
+ $resolver = new AwsS3Resolver($s3, 'images.example.com');
+ $resolver->clear('');
+ }
+
+ protected function getS3ResponseMock($ok = true)
+ {
+ $s3Response = $this->getMock('Guzzle\Service\Resource\Model');
+
+ return $s3Response;
+ }
+}
diff --git a/Tests/Imagine/Cache/Resolver/CacheResolverTest.php b/Tests/Imagine/Cache/Resolver/CacheResolverTest.php
new file mode 100644
index 000000000..bcaff8e09
--- /dev/null
+++ b/Tests/Imagine/Cache/Resolver/CacheResolverTest.php
@@ -0,0 +1,128 @@
+getMockResolver();
+ $resolver
+ ->expects($this->once())
+ ->method('resolve')
+ ->with($request, $this->path, $this->filter)
+ ->will($this->returnValue($this->targetPath))
+ ;
+
+ $cacheResolver = new CacheResolver(new MemoryCache(), $resolver);
+
+ $this->assertEquals($this->targetPath, $cacheResolver->resolve($request, $this->path, $this->filter));
+
+ // Call multiple times to verify the cache is used.
+ $this->assertEquals($this->targetPath, $cacheResolver->resolve($request, $this->path, $this->filter));
+ $this->assertEquals($this->targetPath, $cacheResolver->resolve($request, $this->path, $this->filter));
+ }
+
+ public function testStoreIsForwardedToResolver()
+ {
+ $response = new Response();
+
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->exactly(2))
+ ->method('store')
+ ->with($response, $this->targetPath, $this->filter)
+ ->will($this->returnValue($response))
+ ;
+
+ $cacheResolver = new CacheResolver(new MemoryCache(), $resolver);
+
+ // Call twice, as this method should not be cached.
+ $this->assertSame($response, $cacheResolver->store($response, $this->targetPath, $this->filter));
+ $this->assertSame($response, $cacheResolver->store($response, $this->targetPath, $this->filter));
+ }
+
+ public function testGetBrowserPath()
+ {
+ $absolute = 'http://example.com' . $this->targetPath;
+ $relative = $this->targetPath;
+
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->at(0))
+ ->method('getBrowserPath')
+ ->with($this->path, $this->filter, true)
+ ->will($this->returnValue($absolute))
+ ;
+ $resolver
+ ->expects($this->at(1))
+ ->method('getBrowserPath')
+ ->with($this->path, $this->filter, false)
+ ->will($this->returnValue($relative))
+ ;
+
+ $cacheResolver = new CacheResolver(new MemoryCache(), $resolver);
+
+ $this->assertEquals($absolute, $cacheResolver->getBrowserPath($this->path, $this->filter, true));
+ $this->assertEquals($absolute, $cacheResolver->getBrowserPath($this->path, $this->filter, true));
+
+ $this->assertEquals($relative, $cacheResolver->getBrowserPath($this->path, $this->filter, false));
+ $this->assertEquals($relative, $cacheResolver->getBrowserPath($this->path, $this->filter, false));
+ }
+
+ /**
+ * @depends testResolveIsSavedToCache
+ */
+ public function testRemoveUsesIndex()
+ {
+ $request = new Request();
+
+ $resolver = $this->getMockResolver();
+ $resolver
+ ->expects($this->once())
+ ->method('resolve')
+ ->with($request, $this->path, $this->filter)
+ ->will($this->returnValue($this->targetPath))
+ ;
+ $resolver
+ ->expects($this->once())
+ ->method('remove')
+ ->will($this->returnValue(true))
+ ;
+
+ $cache = new MemoryCache();
+
+ $cacheResolver = new CacheResolver($cache, $resolver);
+ $cacheResolver->resolve($request, $this->path, $this->filter);
+
+ /*
+ * Three items:
+ * * The result of resolve.
+ * * The result of reverse for the targetPath.
+ * * The index of both entries.
+ */
+ $this->assertCount(3, $cache->data);
+
+ $this->assertTrue($cacheResolver->remove($this->targetPath, $this->filter));
+
+ // Cache including index has been removed.
+ $this->assertCount(0, $cache->data);
+ }
+}
diff --git a/Tests/Imagine/Cache/Resolver/WebPathResolverTest.php b/Tests/Imagine/Cache/Resolver/WebPathResolverTest.php
new file mode 100644
index 000000000..fcc96be5c
--- /dev/null
+++ b/Tests/Imagine/Cache/Resolver/WebPathResolverTest.php
@@ -0,0 +1,275 @@
+config = $this->getMockFilterConfiguration();
+ $this->config
+ ->expects($this->any())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'cache' => null,
+ )))
+ ;
+
+ $this->webRoot = $this->tempDir.'/root/web';
+ $this->dataRoot = $this->fixturesDir.'/assets';
+ $this->cacheDir = $this->webRoot.'/media/cache';
+
+ $this->filesystem->mkdir($this->cacheDir);
+
+ $this->cacheManager = $this->getMock('Liip\ImagineBundle\Imagine\Cache\CacheManager', array(
+ 'generateUrl',
+ ), array(
+ $this->config, $this->getMockRouter(), $this->webRoot, 'web_path'
+ ));
+
+ $this->resolver = new WebPathResolver($this->filesystem);
+ $this->cacheManager->addResolver('web_path', $this->resolver);
+ }
+
+ public function testDefaultBehavior()
+ {
+ $this->cacheManager
+ ->expects($this->atLeastOnce())
+ ->method('generateUrl')
+ ->will($this->returnValue(str_replace('/', DIRECTORY_SEPARATOR, '/media/cache/thumbnail/cats.jpeg')))
+ ;
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBaseUrl')
+ ->will($this->returnValue('/app.php'))
+ ;
+
+ // Resolve the requested image for the given filter.
+ $targetPath = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ // The realpath() is important for filesystems that are virtual in some way (encrypted, different mount options, ..)
+ $this->assertEquals(str_replace('/', DIRECTORY_SEPARATOR, realpath($this->cacheDir).'/thumbnail/cats.jpeg'), $targetPath,
+ '->resolve() correctly converts the requested file into target path within webRoot.');
+ $this->assertFalse(file_exists($targetPath),
+ '->resolve() does not create the file within the target path.');
+
+ // Store the cached version of that image.
+ $content = file_get_contents($this->dataRoot.'/cats.jpeg');
+ $response = new Response($content);
+ $this->resolver->store($response, $targetPath, 'thumbnail');
+ $this->assertEquals(201, $response->getStatusCode(),
+ '->store() alters the HTTP response code to "201 - Created".');
+ $this->assertTrue(file_exists($targetPath),
+ '->store() creates the cached image file to be served.');
+ $this->assertEquals($content, file_get_contents($targetPath),
+ '->store() writes the content of the original Response into the cache file.');
+
+ // Remove the cached image.
+ $this->assertTrue($this->resolver->remove($targetPath, 'thumbnail'),
+ '->remove() reports removal of cached image file correctly.');
+ $this->assertFalse(file_exists($targetPath),
+ '->remove() actually removes the cached file from the filesystem.');
+ }
+
+ /**
+ * @depends testDefaultBehavior
+ */
+ public function testMissingRewrite()
+ {
+ $this->cacheManager
+ ->expects($this->atLeastOnce())
+ ->method('generateUrl')
+ ->will($this->returnValue('/media/cache/thumbnail/cats.jpeg'))
+ ;
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBaseUrl')
+ ->will($this->returnValue(''))
+ ;
+
+ // The file has already been cached by this resolver.
+ $targetPath = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->filesystem->mkdir(dirname($targetPath));
+ file_put_contents($targetPath, file_get_contents($this->dataRoot.'/cats.jpeg'));
+
+ $response = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response,
+ '->resolve() returns a Response instance if the target file already exists.');
+ $this->assertEquals(302, $response->getStatusCode(),
+ '->resolve() returns the HTTP response code "302 - Found".');
+ $this->assertEquals('/media/cache/thumbnail/cats.jpeg', $response->headers->get('Location'),
+ '->resolve() returns the expected Location of the cached image.');
+ }
+
+ /**
+ * @depends testMissingRewrite
+ */
+ public function testMissingRewriteWithBaseUrl()
+ {
+ $this->cacheManager
+ ->expects($this->atLeastOnce())
+ ->method('generateUrl')
+ ->will($this->returnValue('/app_dev.php/media/cache/thumbnail/cats.jpeg'))
+ ;
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBaseUrl')
+ ->will($this->returnValue('/app_dev.php'))
+ ;
+
+ // The file has already been cached by this resolver.
+ $targetPath = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->filesystem->mkdir(dirname($targetPath));
+ file_put_contents($targetPath, file_get_contents($this->dataRoot.'/cats.jpeg'));
+
+ $response = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response,
+ '->resolve() returns a Response instance if the target file already exists.');
+ $this->assertEquals(302, $response->getStatusCode(),
+ '->resolve() returns the HTTP response code "302 - Found".');
+ $this->assertEquals('/media/cache/thumbnail/cats.jpeg', $response->headers->get('Location'),
+ '->resolve() returns the expected Location of the cached image.');
+ }
+
+ /**
+ * @depends testDefaultBehavior
+ */
+ public function testResolveWithBasePath()
+ {
+ $this->cacheManager
+ ->expects($this->atLeastOnce())
+ ->method('generateUrl')
+ ->will($this->returnValue(str_replace('/', DIRECTORY_SEPARATOR, '/sandbox/app_dev.php/media/cache/thumbnail/cats.jpeg')))
+ ;
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBaseUrl')
+ ->will($this->returnValue(str_replace('/', DIRECTORY_SEPARATOR, '/sandbox/app_dev.php')))
+ ;
+
+ // Resolve the requested image for the given filter.
+ $targetPath = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ // The realpath() is important for filesystems that are virtual in some way (encrypted, different mount options, ..)
+ $this->assertEquals(str_replace('/', DIRECTORY_SEPARATOR, realpath($this->cacheDir).'/thumbnail/cats.jpeg'), $targetPath,
+ '->resolve() correctly converts the requested file into target path within webRoot.');
+ $this->assertFalse(file_exists($targetPath),
+ '->resolve() does not create the file within the target path.');
+
+ // Store the cached version of that image.
+ $content = file_get_contents($this->dataRoot.'/cats.jpeg');
+ $response = new Response($content);
+ $this->resolver->store($response, $targetPath, 'thumbnail');
+ $this->assertEquals(201, $response->getStatusCode(),
+ '->store() alters the HTTP response code to "201 - Created".');
+ $this->assertTrue(file_exists($targetPath),
+ '->store() creates the cached image file to be served.');
+ $this->assertEquals($content, file_get_contents($targetPath),
+ '->store() writes the content of the original Response into the cache file.');
+
+ // Remove the cached image.
+ $this->assertTrue($this->resolver->remove($targetPath, 'thumbnail'),
+ '->remove() reports removal of cached image file correctly.');
+ $this->assertFalse(file_exists($targetPath),
+ '->remove() actually removes the cached file from the filesystem.');
+ }
+
+ /**
+ * @depends testMissingRewrite
+ * @depends testResolveWithBasePath
+ */
+ public function testMissingRewriteWithBasePathWithScriptname()
+ {
+ $this->cacheManager
+ ->expects($this->atLeastOnce())
+ ->method('generateUrl')
+ ->will($this->returnValue('/sandbox/app_dev.php/media/cache/thumbnail/cats.jpeg'))
+ ;
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBasePath')
+ ->will($this->returnValue('/sandbox'))
+ ;
+ $request
+ ->expects($this->atLeastOnce())
+ ->method('getBaseUrl')
+ ->will($this->returnValue('/sandbox/app_dev.php'))
+ ;
+
+ // The file has already been cached by this resolver.
+ $targetPath = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->filesystem->mkdir(dirname($targetPath));
+ file_put_contents($targetPath, file_get_contents($this->dataRoot.'/cats.jpeg'));
+
+ $response = $this->resolver->resolve($request, 'cats.jpeg', 'thumbnail');
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response,
+ '->resolve() returns a Response instance if the target file already exists.');
+ $this->assertEquals(302, $response->getStatusCode(),
+ '->resolve() returns the HTTP response code "302 - Found".');
+ $this->assertEquals('/sandbox/media/cache/thumbnail/cats.jpeg', $response->headers->get('Location'),
+ '->resolve() returns the expected Location of the cached image.');
+ }
+
+ public function testClear()
+ {
+ $filename = $this->cacheDir.'/thumbnails/cats.jpeg';
+ $this->filesystem->mkdir(dirname($filename));
+ file_put_contents($filename, '42');
+ $this->assertTrue(file_exists($filename));
+
+ $this->resolver->clear('/media/cache');
+
+ $this->assertFalse(file_exists($filename));
+ }
+
+ public function testClearWithoutPrefix()
+ {
+ $filename = $this->cacheDir.'/thumbnails/cats.jpeg';
+ $this->filesystem->mkdir(dirname($filename));
+ file_put_contents($filename, '42');
+ $this->assertTrue(file_exists($filename));
+
+ try {
+ // This would effectively clear the web root.
+ $this->resolver->clear('');
+
+ $this->fail('Clear should not work without a valid cache prefix');
+ } catch (\Exception $e) { }
+
+ $this->assertTrue(file_exists($filename));
+ }
+}
diff --git a/Tests/Imagine/Data/DataManagerTest.php b/Tests/Imagine/Data/DataManagerTest.php
new file mode 100644
index 000000000..5e10ffc20
--- /dev/null
+++ b/Tests/Imagine/Data/DataManagerTest.php
@@ -0,0 +1,68 @@
+getMockLoader();
+ $loader
+ ->expects($this->once())
+ ->method('find')
+ ->with('cats.jpeg')
+ ;
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->once())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'data_loader' => null,
+ )))
+ ;
+
+ $dataManager = new DataManager($config, 'default');
+ $dataManager->addLoader('default', $loader);
+
+ $dataManager->find('thumbnail', 'cats.jpeg');
+ }
+
+ public function testFindWithoutLoader()
+ {
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->once())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ 'data_loader' => null,
+ )))
+ ;
+
+ $dataManager = new DataManager($config);
+
+ $this->setExpectedException('InvalidArgumentException', 'Could not find data loader for "thumbnail" filter type');
+ $dataManager->find('thumbnail', 'cats.jpeg');
+ }
+
+ protected function getMockLoader()
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Data\Loader\LoaderInterface');
+ }
+}
diff --git a/Tests/Imagine/Data/Loader/FileSystemLoaderTest.php b/Tests/Imagine/Data/Loader/FileSystemLoaderTest.php
new file mode 100644
index 000000000..98c5073bf
--- /dev/null
+++ b/Tests/Imagine/Data/Loader/FileSystemLoaderTest.php
@@ -0,0 +1,104 @@
+imagine = $this->getMockImagine();
+ }
+
+ /**
+ * @dataProvider invalidPathProvider
+ */
+ public function testFindInvalidPath($path)
+ {
+ $loader = new FileSystemLoader($this->imagine, array(), $this->fixturesDir.'/assets');
+
+ $this->setExpectedException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException');
+
+ $loader->find($path);
+ }
+
+ public function testFindNotExisting()
+ {
+ $this->imagine
+ ->expects($this->never())
+ ->method('open')
+ ;
+
+ $loader = new FileSystemLoader($this->imagine, array('jpeg'), $this->tempDir);
+
+ $file = realpath($this->tempDir).'/invalid.jpeg';
+ $this->setExpectedException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException', 'Source image not found in "'.$file.'"');
+
+ $loader->find('/invalid.jpeg');
+ }
+
+ public function testFindWithNoExtensionDoesNotThrowNotice()
+ {
+ $loader = new FileSystemLoader($this->imagine, array(), $this->tempDir);
+
+ $this->setExpectedException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException');
+
+ $loader->find('/invalid');
+ }
+
+ public function testFindRetrievesImage()
+ {
+ $image = $this->getMockImage();
+
+ $this->imagine
+ ->expects($this->once())
+ ->method('open')
+ ->with(realpath($this->fixturesDir.'/assets/cats.jpeg'))
+ ->will($this->returnValue($image))
+ ;
+
+ $loader = new FileSystemLoader($this->imagine, array('jpeg'), $this->fixturesDir.'/assets');
+ $this->assertSame($image, $loader->find('/cats.jpeg'));
+ }
+
+ public function testFindGuessesFormat()
+ {
+ $image = $this->getMockImage();
+
+ $this->imagine
+ ->expects($this->once())
+ ->method('open')
+ ->with(realpath($this->fixturesDir.'/assets/cats.jpeg'))
+ ->will($this->returnValue($image))
+ ;
+
+ $loader = new FileSystemLoader($this->imagine, array('jpeg'), $this->fixturesDir.'/assets');
+ $this->assertSame($image, $loader->find('/cats.jpg'));
+ }
+
+ public function testFindFileWithoutExtension()
+ {
+ $image = $this->getMockImage();
+
+ $this->filesystem->copy($this->fixturesDir.'/assets/cats.jpeg', $this->tempDir.'/cats');
+
+ $this->imagine
+ ->expects($this->once())
+ ->method('open')
+ ->with(realpath($this->tempDir.'/cats'))
+ ->will($this->returnValue($image))
+ ;
+
+ $loader = new FileSystemLoader($this->imagine, array(), $this->tempDir);
+ $this->assertSame($image, $loader->find('/cats.jpeg'));
+ }
+}
diff --git a/Tests/Imagine/Data/Loader/StreamLoaderTest.php b/Tests/Imagine/Data/Loader/StreamLoaderTest.php
new file mode 100644
index 000000000..22331c10c
--- /dev/null
+++ b/Tests/Imagine/Data/Loader/StreamLoaderTest.php
@@ -0,0 +1,73 @@
+imagine = $this->getMockImagine();
+ }
+
+ public function testFindInvalidFile()
+ {
+ $this->imagine
+ ->expects($this->never())
+ ->method('load')
+ ;
+
+ $loader = new StreamLoader($this->imagine, 'file://');
+
+ $this->setExpectedException('Symfony\Component\HttpKernel\Exception\NotFoundHttpException');
+ $loader->find($this->tempDir.'/invalid.jpeg');
+ }
+
+ public function testFindLoadsFile()
+ {
+ $image = $this->getMockImage();
+
+ $this->imagine
+ ->expects($this->once())
+ ->method('load')
+ ->with(file_get_contents($this->fixturesDir.'/assets/cats.jpeg'))
+ ->will($this->returnValue($image))
+ ;
+
+ $loader = new StreamLoader($this->imagine, 'file://');
+ $this->assertSame($image, $loader->find($this->fixturesDir.'/assets/cats.jpeg'));
+ }
+
+ public function testFindWithContext()
+ {
+ $image = $this->getMockImage();
+
+ $this->imagine
+ ->expects($this->once())
+ ->method('load')
+ ->with(file_get_contents($this->fixturesDir.'/assets/cats.jpeg'))
+ ->will($this->returnValue($image))
+ ;
+
+ $context = stream_context_create();
+
+ $loader = new StreamLoader($this->imagine, 'file://', $context);
+ $this->assertSame($image, $loader->find($this->fixturesDir.'/assets/cats.jpeg'));
+ }
+
+ public function testConstructorInvalidContext()
+ {
+ $this->setExpectedException('InvalidArgumentException', 'The given context is no valid resource.');
+
+ new StreamLoader($this->imagine, 'file://', true);
+ }
+}
diff --git a/Tests/Imagine/Data/Transformer/PdfTransformerTest.php b/Tests/Imagine/Data/Transformer/PdfTransformerTest.php
new file mode 100644
index 000000000..b582eb317
--- /dev/null
+++ b/Tests/Imagine/Data/Transformer/PdfTransformerTest.php
@@ -0,0 +1,54 @@
+markTestSkipped('Imagick is not available.');
+ }
+
+ parent::setUp();
+ }
+
+ public function testApplyWritesPng()
+ {
+ $pdfFilename = $this->tempDir.'/cats.pdf';
+ $pngFilename = $pdfFilename.'.png';
+
+ $pdf = $this->fixturesDir.'/assets/cats.pdf';
+ $this->filesystem->copy($pdf, $pdfFilename);
+ $this->assertTrue(file_exists($pdfFilename));
+
+ $transformer = new PdfTransformer(new \Imagick());
+ $absolutePath = $transformer->apply($pdfFilename);
+
+ $this->assertEquals($pngFilename, $absolutePath);
+ $this->assertTrue(file_exists($pngFilename));
+ $this->assertNotEmpty(file_get_contents($pngFilename));
+ }
+
+ public function testApplyDoesNotOverwriteExisting()
+ {
+ $pdfFilename = $this->tempDir.'/cats.pdf';
+ $pngFilename = $pdfFilename.'.png';
+ $this->filesystem->touch(array(
+ $pdfFilename,
+ $pngFilename,
+ ));
+
+ $transformer = new PdfTransformer(new \Imagick());
+ $absolutePath = $transformer->apply($pdfFilename);
+
+ $this->assertEquals($pngFilename, $absolutePath);
+ $this->assertEmpty(file_get_contents($pngFilename));
+ }
+}
diff --git a/Tests/Imagine/Filter/FilterConfigurationTest.php b/Tests/Imagine/Filter/FilterConfigurationTest.php
new file mode 100644
index 000000000..763d52328
--- /dev/null
+++ b/Tests/Imagine/Filter/FilterConfigurationTest.php
@@ -0,0 +1,38 @@
+ array(
+ 'thumbnail' => array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ ),
+ ),
+ 'cache' => 'web_path',
+ );
+
+ $filterConfiguration = new FilterConfiguration();
+ $filterConfiguration->set('profile_photo', $config);
+
+ $this->assertEquals($config, $filterConfiguration->get('profile_photo'));
+ }
+
+ public function testGetUndefinedFilter()
+ {
+ $filterConfiguration = new FilterConfiguration();
+
+ $this->setExpectedException('RuntimeException', 'Filter not defined: thumbnail');
+ $filterConfiguration->get('thumbnail');
+ }
+}
diff --git a/Tests/Imagine/Filter/FilterManagerTest.php b/Tests/Imagine/Filter/FilterManagerTest.php
new file mode 100644
index 000000000..aa0307b18
--- /dev/null
+++ b/Tests/Imagine/Filter/FilterManagerTest.php
@@ -0,0 +1,223 @@
+getMockFilterConfiguration();
+ $config
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'filters' => array(
+ 'thumbnail' => array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ ),
+ ),
+ )))
+ ;
+ $filterManager = new FilterManager($config);
+
+ $this->setExpectedException('InvalidArgumentException', 'Could not find filter loader for "thumbnail" filter type');
+ $filterManager->get(new Request(), 'thumbnail', $this->getMockImage(), 'cats.jpeg');
+ }
+
+ public function testGetDefaultBehavior()
+ {
+ $thumbConfig = array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ );
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'filters' => array(
+ 'thumbnail' => $thumbConfig,
+ ),
+ )))
+ ;
+
+ $image = $this->getMockImage();
+ $image
+ ->expects($this->once())
+ ->method('get')
+ ->with('jpeg', array('quality' => 100))
+ ->will($this->returnSelf())
+ ;
+
+ $loader = $this->getMockLoader();
+ $loader
+ ->expects($this->once())
+ ->method('load')
+ ->with($image, $thumbConfig)
+ ->will($this->returnValue($image))
+ ;
+
+ $filterManager = new FilterManager($config);
+ $filterManager->addLoader('thumbnail', $loader);
+
+ $request = new Request();
+ $response = $filterManager->get($request, 'thumbnail', $image, $this->fixturesDir.'/assets/cats.jpeg');
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('image/jpeg', $response->headers->get('Content-Type'));
+ }
+
+ public function testGetConfigAltersFormatAndQuality()
+ {
+ $thumbConfig = array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ );
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'filters' => array(
+ 'thumbnail' => $thumbConfig,
+ ),
+ 'format' => 'jpg',
+ 'quality' => 80,
+ )))
+ ;
+
+ $image = $this->getMockImage();
+ $image
+ ->expects($this->once())
+ ->method('get')
+ ->with('jpg', array('quality' => 80))
+ ->will($this->returnSelf())
+ ;
+
+ $loader = $this->getMockLoader();
+ $loader
+ ->expects($this->once())
+ ->method('load')
+ ->with($image, $thumbConfig)
+ ->will($this->returnValue($image))
+ ;
+
+ $filterManager = new FilterManager($config);
+ $filterManager->addLoader('thumbnail', $loader);
+
+ $request = new Request();
+ $response = $filterManager->get($request, 'thumbnail', $image, $this->fixturesDir.'/assets/cats.jpeg');
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('image/jpg', $response->headers->get('Content-Type'));
+ }
+
+ public function testGetRequestKnowsContentType()
+ {
+ $thumbConfig = array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ );
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'filters' => array(
+ 'thumbnail' => $thumbConfig,
+ ),
+ 'format' => 'jpg',
+ )))
+ ;
+
+ $image = $this->getMockImage();
+ $image
+ ->expects($this->once())
+ ->method('get')
+ ->with('jpg', array('quality' => 100))
+ ->will($this->returnSelf())
+ ;
+
+ $loader = $this->getMockLoader();
+ $loader
+ ->expects($this->once())
+ ->method('load')
+ ->with($image, $thumbConfig)
+ ->will($this->returnValue($image))
+ ;
+
+ $filterManager = new FilterManager($config);
+ $filterManager->addLoader('thumbnail', $loader);
+
+ $request = $this->getMock('Symfony\Component\HttpFoundation\Request');
+ $request
+ ->expects($this->once())
+ ->method('getMimeType')
+ ->with('jpg')
+ ->will($this->returnValue('image/jpeg'))
+ ;
+
+ $response = $filterManager->get($request, 'thumbnail', $image, $this->fixturesDir.'/assets/cats.jpeg');
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('image/jpeg', $response->headers->get('Content-Type'));
+ }
+
+ public function testApplyFilterSet()
+ {
+ $image = $this->getMockImage();
+
+ $thumbConfig = array(
+ 'size' => array(180, 180),
+ 'mode' => 'outbound',
+ );
+
+ $config = $this->getMockFilterConfiguration();
+ $config
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->with('thumbnail')
+ ->will($this->returnValue(array(
+ 'filters' => array(
+ 'thumbnail' => $thumbConfig,
+ ),
+ )))
+ ;
+
+ $loader = $this->getMockLoader();
+ $loader
+ ->expects($this->once())
+ ->method('load')
+ ->with($image, $thumbConfig)
+ ->will($this->returnValue($image))
+ ;
+
+ $filterManager = new FilterManager($config);
+ $filterManager->addLoader('thumbnail', $loader);
+
+ $this->assertSame($image, $filterManager->applyFilter($image, 'thumbnail'));
+ }
+
+ protected function getMockLoader()
+ {
+ return $this->getMock('Liip\ImagineBundle\Imagine\Filter\Loader\LoaderInterface');
+ }
+}
diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php
index 56c074c0e..7839509b8 100644
--- a/Tests/bootstrap.php
+++ b/Tests/bootstrap.php
@@ -1,26 +1,8 @@
registerNamespace('Symfony', SYMFONY_SRC_DIR);
-$loader->registerNamespace('Imagine', IMAGINE_SRC_DIR);
-$loader->register();
+$autoload = require_once $file;
diff --git a/composer.json b/composer.json
new file mode 100644
index 000000000..a37aaadea
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,39 @@
+{
+ "name": "liip/imagine-bundle",
+ "type": "symfony-bundle",
+ "description": "This Bundle assists in imagine manipulation using the imagine library",
+ "keywords": ["imagine", "image"],
+ "homepage": "http://liip.ch",
+ "license": "MIT",
+ "minimum-stability": "dev",
+ "authors": [
+ {
+ "name": "Liip and other contributors",
+ "homepage": "https://github.com/liip/LiipImagineBundle/contributors"
+ }
+ ],
+
+ "require": {
+ "php": ">=5.3.2",
+ "imagine/Imagine": "0.5.*",
+ "symfony/finder": ">=2.0.16,~2.0",
+ "symfony/filesystem": ">=2.0.16,~2.0",
+ "symfony/options-resolver": ">=2.0.16,~2.0",
+ "symfony/framework-bundle": ">=2.0.16,~2.0"
+ },
+
+ "require-dev": {
+ "twig/twig": ">=1.0,<2.0-dev",
+ "symfony/yaml": ">=2.0.16,~2.0"
+ },
+
+ "suggest": {
+ "twig/twig": ">=1.0,<2.0-dev"
+ },
+
+ "autoload": {
+ "psr-0": { "Liip\\ImagineBundle": "" }
+ },
+
+ "target-dir": "Liip/ImagineBundle"
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index c22edde42..85d1871a4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,19 +1,10 @@
-
+
+
-
- ./Tests/
+
+ ./Tests
@@ -23,14 +14,8 @@
./Resources
./Tests
+ ./vendor
-
-
-
-
-
-
-