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

5.1k views Asked by At

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

2

There are 2 answers

0
Jozef Kétyi On BEST ANSWER

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 :)

0
Julfiker On
 //config.yml 
 sonata_media:
    contexts:
        default:  # the default context is mandatory
            providers:
                - sonata.media.provider.image

            formats:
                small: { width: 100 , quality: 70}
                big:   { width: 500 , quality: 70}

 //Controller action:
 $media = //$entity->getMedia(); Media entity
 $mediaProvider = $this->get('sonata.media.provider.image'); 
 $format = $mediaProvider->getFormatName($media, 'big');
 $imgPath = $mediaProvider->generatePublicUrl($media, $format);