phpsymfonysonata-media-bundle

How to get Sonata Media Bundle to generate pictures in separate folders


I'm struggling with a problem that I need Sonata Media Bundle to upload pictures to different folders. What I'm trying to reach is to generate a picture structure as the following: if the picture has an ID for 1234567 the original image will be put into folder: "/web/uploads/1234/567/original' And all the generated thumbnails/sizes will be like: "/web/uploads/1234/567/100x130" and "/web/uploads/1234/567/200x100" depending on the generated size.

Is this folder structure generation possible in Sonata Media Bundle and if yes, how?

Thanks for the answers. Jozef


Solution

  • We worked out a "workaround" for this problem:

    Basically what we have done is that we created our own Generator, Provider and Resizer to match our needs and than injected it back to the Media Bundle. The generator implements Generator Interface, the constructor only has one argument ($this->firstLevel = 1000;). Than:

    public function generatePath(MediaInterface $media)
    {
        $rep_first_level = (int) ($media->getId() / $this->firstLevel);
        $rep_second_level = (int) ($media->getId() % $this->firstLevel);
    
        return sprintf('%s/%04s/%03s', $media->getContext(), $rep_first_level, $rep_second_level);
    }
    

    This will create the sub directories in the preferred way "/web/uploads/1234/567" for image with ID "1234567".

    Our provider extends ImageProvider and has only 1 instances:

    protected function generateReferenceName(MediaInterface $media)
    {
        $metadata = $media->getProviderMetadata();
        $fileName = $metadata['filename'];
        $temp = explode('.', $fileName);
        $name = $temp[0];
        return '/origos/' . $name . '.' . $media->getBinaryContent()->getExtension();
    }
    

    Our Resizer class will extend squareResizer. This was changed to be able to generate pictures (thumbnails) the exact size (100x100 and 190x100 also) by cropping the image. For this we created a new resizer which implements ResizerInterface:

    <?php
    
    namespace Sita\<YourBundle>\Resizer;
    
    use Imagine\Image\ImagineInterface;
    use Imagine\Image\Box;
    use Imagine\Image\Point;
    use Gaufrette\File;
    use Sonata\MediaBundle\Model\MediaInterface;
    use Sonata\MediaBundle\Metadata\MetadataBuilderInterface;
    use Sonata\MediaBundle\Resizer\ResizerInterface;
    
    class <YourResizer> implements ResizerInterface
    {
        /**
         * ImagineInterface
         */
        protected $adapter;
    
        /**
         * string
         */
        protected $mode;
    
        /**
         * @param ImagineInterface $adapter
         * @param string $mode
         * @param MetadataBuilderInterface $metadata
         */
        public function __construct(ImagineInterface $adapter, $mode, MetadataBuilderInterface $metadata)
        {
            $this->adapter = $adapter;
            $this->mode    = $mode;
            $this->metadata = $metadata;
        }
    
        /**
         * {@inheritdoc}
         */
        public function resize(MediaInterface $media, File $in, File $out, $format, array $settings)
        {
            if (!isset($settings['width'])) {
                throw new \RuntimeException(sprintf('Width parameter is missing in context "%s" for provider "%s"', $media->getContext(), $media->getProviderName()));
            }
    
            $image = $this->adapter->load($in->getContent());
            $size  = $media->getBox();
    
            if (null != $settings['height']) {
                $ratioWidth = $size->getWidth() / $settings['width'];
                $ratioHeight = $size->getHeight() / $settings['height'];
                $ratio = $ratioHeight > $ratioWidth ? $ratioWidth : $ratioHeight;
    
                $point = new Point(($size->getWidth() - $settings['width'] * $ratio) / 2, ($size->getHeight() - $settings['height'] * $ratio) / 2);
    
                $image->crop($point, new Box($settings['width'] * $ratio, $settings['height'] * $ratio));
                $size = $image->getSize();
            }
    
            $settings['height'] = (int) ($settings['width'] * $size->getHeight() / $size->getWidth());
    
            if ($settings['height'] < $size->getHeight() && $settings['width'] < $size->getWidth()) {
                $content = $image
                    ->thumbnail(new Box($settings['width'], $settings['height']), $this->mode)
                    ->get($format, array('quality' => $settings['quality']));
            } else {
                $content = $image->get($format, array('quality' => $settings['quality']));
            }
    
            $out->setContent($content, $this->metadata->get($media, $out->getName()));
        }
    
        /**
         * {@inheritdoc}
         */
        public function getBox(MediaInterface $media, array $settings)
        {
            $size = $media->getBox();
    
            if (null != $settings['height']) {
    
                if ($size->getHeight() > $size->getWidth()) {
                    $higher = $size->getHeight();
                    $lower  = $size->getWidth();
                } else {
                    $higher = $size->getWidth();
                    $lower  = $size->getHeight();
                }
    
                if ($higher - $lower > 0) {
                    return new Box($lower, $lower);
                }
            }
    
            $settings['height'] = (int) ($settings['width'] * $size->getHeight() / $size->getWidth());
    
            if ($settings['height'] < $size->getHeight() && $settings['width'] < $size->getWidth()) {
                return new Box($settings['width'], $settings['height']);
            }
    
            return $size;
        }
    }
    

    It was a bit challenge to do the dependency injection, but here is the result:

    Services:

        parameters:
        <yourBundle>.generator.<project>_generator.class: Sita\<yourBundle>\Generator\<project>Generator
        <yourBundle>.resizer.<project>_resizer.class: Sita\<yourBundle>\Resizer\<project>Resizer
        <yourBundle>.thumbnail.<project>_thumbnail.class: Sita\<yourBundle>\Thumbnail\<project>Thumbnail
        <yourBundle>.provider.<project>_provider.class: Sita\<yourBundle>\Provider\<project>Provider
    services:
        <yourBundle>.generator.<project>_generator:
            class: %<yourBundle>.generator.<project>_generator.class%
            arguments:
                - ~
    
        <yourBundle>.resizer.<project>_resizer:
            class: %<yourBundle>.resizer.<project>_resizer.class%
            arguments:
                - @sonata.media.adapter.image.gd
                - %sonata.media.resizer.square.adapter.mode%
                - @sonata.media.metadata.proxy
    
        <yourBundle>.thumbnail.<project>_thumbnail:
            class: %<yourBundle>.thumbnail.<project>_thumbnail.class%
            arguments:
                - %sonata.media.thumbnail.format.default%
    
        <yourBundle>.provider.<project>:
            class: %<yourBundle>.provider.<project>_provider.class%
            arguments:
                - <yourBundle>.provider.<project>
                - ~
                - ~
                - ~
                - @<yourBundle>.thumbnail.<project>_thumbnail
                - ~
                - ~
                - ~
                - @sonata.media.metadata.proxy
            calls:
                - [setResizer, ["@<yourBundle>.resizer.<project>_resizer"]]
            tags:
                - { name: sonata.media.provider }
    

    Configuration:

    <?php
    
    namespace <yourBundle>\DependencyInjection;
    
    use Symfony\Component\Config\Definition\Builder\TreeBuilder;
    use Symfony\Component\Config\Definition\ConfigurationInterface;
    
    /**
     * This is the class that validates and merges configuration from your app/config files
     *
     * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
     */
    class Configuration implements ConfigurationInterface
    {
        /**
         * {@inheritdoc}
         */
        public function getConfigTreeBuilder()
        {
            $treeBuilder = new TreeBuilder();
            $rootNode = $treeBuilder->root('<yourBundle>');
    
            $rootNode
                ->children()
                    ->arrayNode('providers')
                        ->addDefaultsIfNotSet()
                        ->children()
                            ->arrayNode('<project>')
                                ->addDefaultsIfNotSet()
                                ->children()
                                    ->scalarNode('service')->defaultValue('<yourBundle>.provider.<project>')->end()
                                    ->scalarNode('resizer')->defaultValue('<yourBundle>.resizer.<project>_resizer')->end()
                                    ->scalarNode('filesystem')->defaultValue('sonata.media.filesystem.local')->end()
                                    ->scalarNode('cdn')->defaultValue('sonata.media.cdn.server')->end()
                                    ->scalarNode('generator')->defaultValue('<yourBundle>.generator.<project>_generator')->end()
                                    ->scalarNode('thumbnail')->defaultValue('<yourBundle>.thumbnail.<project>_thumbnail')->end()
                                    ->scalarNode('adapter')->defaultValue('sonata.media.adapter.image.gd')->end()
                                    ->arrayNode('allowed_extensions')
                                        ->prototype('scalar')->end()
                                        ->defaultValue(array('jpg', 'png', 'jpeg'))
                                    ->end()
                                    ->arrayNode('allowed_mime_types')
                                        ->prototype('scalar')->end()
                                        ->defaultValue(array(
                                            'image/pjpeg',
                                            'image/jpeg',
                                            'image/png',
                                            'image/x-png',
                                        ))
                                    ->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end();
    
            return $treeBuilder;
        }
    }
    

    and Extension:

    <?php
    
    namespace <yourBundle>\DependencyInjection;
    
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\Config\FileLocator;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\HttpKernel\DependencyInjection\Extension;
    use Symfony\Component\DependencyInjection\Loader;
    
    /**
     * This is the class that loads and manages your bundle configuration
     *
     * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
     */
    class <yourBundle>Extension extends Extension
    {
        /**
         * {@inheritdoc}
         */
        public function load(array $configs, ContainerBuilder $container)
        {
            $configuration = new Configuration();
            $config = $this->processConfiguration($configuration, $configs);
    
            $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
            $loader->load('services.yml');
    
            $container->getDefinition('<yourBundle>.provider.<project>')
                ->replaceArgument(1, new Reference($config['providers']['<project>']['filesystem']))
                ->replaceArgument(2, new Reference($config['providers']['<project>']['cdn']))
                ->replaceArgument(3, new Reference($config['providers']['<project>']['generator']))
                ->replaceArgument(4, new Reference($config['providers']['<project>']['thumbnail']))
                ->replaceArgument(5, array_map('strtolower', $config['providers']['<project>']['allowed_extensions']))
                ->replaceArgument(6, $config['providers']['<project>']['allowed_mime_types'])
                ->replaceArgument(7, new Reference($config['providers']['<project>']['adapter']))
            ;
    
        }
    }
    

    And finally the config.yml from the symfony config file:

    sonata_media:
        # if you don't use default namespace configuration
        #class:
        #    media: MyVendor\MediaBundle\Entity\Media
        #    gallery: MyVendor\MediaBundle\Entity\Gallery
        #    gallery_has_media: MyVendor\MediaBundle\Entity\GalleryHasMedia
        default_context: default
        db_driver: doctrine_orm # or doctrine_mongodb, doctrine_phpcr
        contexts:
            default:  # the default context is mandatory
                providers:
                    - <yourBundle>.provider.<project>
    
                formats:
                    small: { width: 100 , quality: 70}
                    big:   { width: 500 , quality: 70}
                    100x100: { width: 100 , height: 100 , quality: 100 }
                    126x190: { width: 126 , height: 190 , quality: 100 }
                    190x126: { width: 190 , height: 126 , quality: 100 }
                    190x56: { width: 190 , height: 56 , quality: 100 }
    
        cdn:
            server:
                path: /uploads/media # http://media.sonata-project.org/
    
        filesystem:
            local:
                directory:  %kernel.root_dir%/../web/uploads/media
                create:     true
    
    <yourBundle>:
    

    I know this is not the clearest work, but it does the job, good for now :)