As we know www is the public web root of symfony framework so if you keep your images in there, they are accessible by anyone which is not a problem in certain cases. However, you'll sometimes need to keep some kind of images in private and prevent people seing them unless they have access rights. In this example we are going to upload images to a private directory app/Resources/private/uploads/images and serve them in twig templates with a controller for security reasons.


Logic


Files will be uploaded to app/Resources/private/uploads/images. When it comes to serving files, all we have to do is to call controller endpoint in twig file with <img src="{{ path('image_show', {'name': image_name}) }}" />. This will then look like <img src="/app_dev.php/6f3f48c823a61a2919ad6092ffe0f1e0.png"> in HTML source so the actual image path is not exposed at all.


Permissions


$ HTTPDUSER=`ps axo user,comm | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\  -f1`
$ sudo setfacl -R -m u:"$HTTPDUSER":rwX -m u:`whoami`:rwX app/Resources/private/uploads/images
$ sudo setfacl -dR -m u:"$HTTPDUSER":rwX -m u:`whoami`:rwX app/Resources/private/uploads/images

parameters.yml


parameters:
image_upload_path: '%kernel.root_dir%/Resources/private/uploads/images'

routing.yml


inanzzz_application:
resource: "@InanzzzApplicationBundle/Controller/"
type: annotation
prefix: /

ImageController


Just to keep the example as short as possible, I'm keeping whole logic in controller but remember it is not a good practise.


namespace Inanzzz\ApplicationBundle\Controller;

use Inanzzz\ApplicationBundle\Form\Model\ImageUpload as ImageUploadModel;
use Inanzzz\ApplicationBundle\Form\Type\ImageUploadType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\RouterInterface;

/**
* @Route("", service="inanzzz_application.controller.image")
*/
class ImageController
{
private $templating;
private $formFactory;
private $router;
private $imageUploadPath;

public function __construct(
EngineInterface $templating,
FormFactoryInterface $formFactory,
RouterInterface $router,
$imageUploadPath
) {
$this->templating = $templating;
$this->formFactory = $formFactory;
$this->router = $router;
$this->imageUploadPath = $imageUploadPath;
}

/**
* @Method({"GET"})
* @Route("/", name="image_index")
*
* @return Response
*/
public function indexAction()
{
return $this->templating->renderResponse(
'InanzzzApplicationBundle:Image:index.html.twig',
[
'form' => $this->createForm()->createView(),
'images' => $this->getImages(),
]
);
}

/**
* @param string $name
*
* @Method({"GET"})
* @Route("/{name}", name="image_show")
*
* @return Response
*/
public function showAction($name)
{
$response = new BinaryFileResponse($this->imageUploadPath.'/'.$name);
$response->trustXSendfileTypeHeader();
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$name,
iconv('UTF-8', 'ASCII//TRANSLIT', $name)
);

return $response;
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("/upload/image", name="image_upload_image")
*
* @return Response
*/
public function uploadImageAction(Request $request)
{
$imageUploadModel = new ImageUploadModel();

$form = $this->createForm($imageUploadModel);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {

/** @var UploadedFile $image */
$image = $imageUploadModel->image;
$name = md5(uniqid().microtime().$image->getClientOriginalName()).'.'.$image->guessExtension();

$image->move($this->imageUploadPath, $name);
}

return new RedirectResponse($this->router->generate('image_index'));
}

private function createForm(ImageUploadModel $imageUploadModel = null)
{
return $this->formFactory->create(
ImageUploadType::class,
$imageUploadModel ? $imageUploadModel : new ImageUploadModel(),
[
'method' => 'POST',
'action' => $this->router->generate('image_upload_image')
]
);
}

private function getImages()
{
$images = [];

$finder = new Finder();
$finder->files()->in($this->imageUploadPath);

/** @var SplFileInfo $file */
foreach ($finder as $file) {
$images[] = $file->getFilename();
}

return $images;
}
}

This is your controller service definition.


services:
inanzzz_application.controller.image:
class: Inanzzz\ApplicationBundle\Controller\ImageController
arguments:
- '@templating'
- '@form.factory'
- '@router'
- '%image_upload_path%'

index.html.twig


{% block body %}
<p>Upload!</p>

<hr />
{{ form_start(form) }}
{{ form_row(form.image) }}
<button type="submit">Upload</button>
{{ form_end(form) }}

<hr />
{% for image in images %}
<img src="{{ path('image_show', {'name': image}) }}" />
{% else %}
<em>No image found.</em>
{% endfor %}
{% endblock %}

ImageUploadType


namespace Inanzzz\ApplicationBundle\Form\Type;

use Inanzzz\ApplicationBundle\Form\Model\ImageUpload as ImageUploadModel;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;

class ImageUploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', FileType::class, ['label' => 'Select an image']);
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => ImageUploadModel::class]);
}
}

ImageUpload


I am not validating the user input but you definitelly should. Again, keeping things short for now.


namespace Inanzzz\ApplicationBundle\Form\Model;

class ImageUpload
{
public $image;
}