Managing media in an eZ Publish 4 install was always a pain point for me. There were a number of problems that I had with media in eZ Publish 4 that are now addressed thanks to Symfony. The media I am referring to in this post are any content types placed in the media category, traditionally images, files, audio files, and video files.

Problem Number 1, the content download

Anyone familiar with eZ Publish 4 knows that in order force a download of a media content object you needed to build a route to the content/download module/view. The content/download was very handy in its day because it did its job well, it forced a download on whatever file it was given. content/download even handled permissions for the file. It also, though, had a number of problems. The first problem with the content/download has to do with how developers were forced to build downloads.

An example download url built for eZ Publish 4:

{def $download_url = concat( "content/download/", $attribute.contentobject_id, "/", $attribute.content.contentobject_attribute_id, "/", $attribute.content.original_filename )|ezurl}

Hopefully the problems are obvious, not only is the url a pain to build, but the url is subject to change. That means that whenever an editor changes the uploaded file attached to the content, a new download url will be generated. This means that these urls can never be used outside the generated site pages (no emails, etc), unless you are OK with possible dead links from time to time.

The second problem I have with the content/download method is that the developer gets no control over the title of the downloaded file. The downloaded file will always have the name of the uploaded file. Most of the time this is fine, but when fine tuning is needed, the content/download is not flexible.

Problem Number 2, the media full view

I have yet to have a client want the full view of a media content object include the site chrome. In eZ Publish 4, this was a pain point we just had to live with. The generated URL Alias for media content went to a full view of the content, by default with the site chrome but this can be changed. What could not be changed in eZ Publish 4 were the headers when viewing full view media. So, though I could adjust the chrome of a media full view, I could never do anything but display the content to the user using the default site headers.

The Solution, A Symfony media controller

I am left with two problems to solve, luckily the same solution fixes both. In eZ Publish 5 (and eZ Platform) developers can set custom controllers when matching content to templates. Providing a custom Symfony controller and action gives developers the control needed to set the necessary headers to force a download while providing a consistent path to file downloads.

First, set the full view to use a new MediaController

system:
    global:
        location_view:
            full:
                media:
                    controller: "AppBundle:Media:media"
                    match:
                        Identifier\ContentType: [ file, image, video ]

Second, create the MediaController

What this does is lets a user pass a dl query parameter to the eZ generated url alias. The query parameter tells then tells Symfony which header generate for the response, either download or view the media. This means that the generated eZ url alias will be the url to the media file and not the one subject to change, it also gets rid of the chrome around the image since we are serving the image directly.

<?php

namespace AppBundle\Controller;

use eZ\Bundle\EzPublishCoreBundle\Controller;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use eZ\Publish\SPI\FieldType\Value;

class MediaController extends Controller
{
    /**
     * @param $locationId
     * @param $viewType
     * @param bool $layout
     * @param array $params
     *
     * @return Response
     */
    public function mediaAction($locationId, $viewType, $layout = false, array $params = array())
    {
        $disposition = $this->getDisposition();
        $file = $this->getFile($locationId);
        $response = $this->getBinaryFileResponse($file, $disposition);

        return $response;
    }

    /**
     * @return ResponseHeaderBag::DISPOSITION_ATTACHMENT|ResponseHeaderBag::DISPOSITION_INLINE
     */
    public function getDisposition()
    {
        $request = $this->get('request_stack')->getCurrentRequest();
        $dl = $request->get('dl');
        return $dl == 1 ? ResponseHeaderBag::DISPOSITION_ATTACHMENT : ResponseHeaderBag::DISPOSITION_INLINE;
    }

    /**
     * @param $locationId
     * @param string $fileName
     *
     * @return mixed
     */
    private function getFile($locationId, $fileName = 'file')
    {
        $location = $this->getRepository()->getLocationService()->loadLocation($locationId);
        $content = $this->getRepository()->getContentService()->loadContent($location->contentId);
        $file = $content->getFieldValue($fileName);

        return $file;
    }

    /**
     * @param $file
     * @param string $dispositionType
     *
     * @return BinaryFileResponse
     */
    private function getBinaryFileResponse(Value $file, $dispositionType = ResponseHeaderBag::DISPOSITION_ATTACHMENT)
    {
        $fileUri = $this->container->getParameter('kernel.root_dir') . '/../web' . $file->uri;
        $response = new BinaryFileResponse($fileUri);
        $response->setContentDisposition($dispositionType, $file->fileName);

        return $response;
    }
}

Conclusion

I am sure there are other options for handling media that I haven’t though of, please share your ideas as I, for one, would love to hear them. With Symfony there is no one size fits all solution, so this MediaController may not work as designed. That said, I do hope this helps with managing media inside eZ Publish/Platform in the future. Please note that I do not show any permissions customizations in the MediaController, some permissions will still be automatically managed by eZ, but you will probably have to include some permissions checks in your actions.