Building an Ad Manager in Symfony 2

Share this article

Just this once won’t hurt – I am not going to write about Sass but Symfony. I had to do a little bit of backend at work and ended up with an interesting problem to solve, involving quite a lot of things so I thought it wouldn’t be such a bad idea to write an article about it.

But first, let me explain. The main idea was to build an ad manager. What the hell is an ad manager you say? Let’s say you have some places on your site/application to display ads. We do have things like this on our site, and one of our teams is (partially) dedicated to bringing those places to life with content.

Now for some boring reasons I won’t list here, we couldn’t use an existing tool, so we were doomed to build something from scratch. As usual, we wanted to do a lot without much coding, while keeping an overall simplicity for the end user (who is not a developer). I think we came up with a fairly decent solution for our little project.

Here are the features we set up:

  • YAML configuration + FTP access;
  • Either images, videos or HTML content;
  • Ability to customize cache duration;
  • Either sliders (yes, the pattern sucks) or random item in collection.

The idea is quite simple. In Twig templates, we use render_esi (more info here) hitting on a controller action passing it a unique key (basically the name of the ad spot), for instance:

{{ render_esi(url('ads_manager', { 'id': 'home_sidebar_spot' })) }}

Then the action fetches the YAML configuration file, grabs the data associated with the given id, and renders a template. At this point, the template does some very simple logic depending on which type of content the given items are (images, videos, HTML…).

Ready? Let’s go.

Global configuration

There are two things we need to have in a global configuration (likely the parameters.yml file): path to the configuration file (ads.yml), and an array of allowed media types.

ads:
    uri: http://location.com/path/to/ads.yml
    allowed_types: ['image', 'video', 'html']

The configuration file

The configuration file is maintained by the team in charge of making the ads. YAML is a human-friendly language that’s perfect for when it comes to simple configuration.

This file is built like this:

home_sidebar_spot:
    cache_public: true
    cache_shared_max_age: 86400
    cache_max_age: 28800
    random: true
    data: 
        - type: "image"
          link: "http://cdn.domain.tld/path/to/file.png"
          target: "http://google.fr/"
          weight: 1

Where:

  • cache_public defines whether the cache should public or private;
  • cache_shared_max_age defines the max age for the cache on the server network;
  • cache_max_age defines the max age for the cache on the client browser;
  • random defines whether the spot should be treated as a slider (multiple items coming one after another) or a static item, randomly chosen;
  • data is an array of items, either all displayed if random is false or reduced to a single item if random is true.

Each item from data is an object composed of:

  • type defines the media type, either image, video or html;
  • link defines the media content so an absolute URL to an image file, a video service or an HTML file;
  • target defines the target of the link behind the image if type is image, otherwise is ignored;
  • weight defines a boost when data contains several items and random is set to true.

And there is such a block for every ad spot on our site, so basically a dozen of those.

The Controller

The controller is very simple: it has a single action. The scenario is:

  1. fetch configuration file;
  2. parse it;
  3. find data from given id;
  4. set cache configuration;
  5. reduce to a single item if it’s random;
  6. render view.
<?php
// Namespace and uses
class AdsManagerController extends Controller
{

    /**
     * @Route("/ads_manager/{id}", name="ads_manager")
     */
    public function indexAction ($id)
    {
        // Fetch data
        $data = $this->getData($id);

        // Prepare response
        $response = new Response();

        // Configure cache
        if ($data['cache_public'] === true) {
            $response->setPublic();
        } else {
            $response->setPrivate();
        }

        // Set max age
        $response->setMaxAge($data['cache_max_age']);
        $response->setSharedMaxAge($data['cache_shared_max_age']);

        // Handle the weight random
        if ($data['random'] === true) {
            $data['data'] = [$this->randomItem($data['data'])];
        }

        // If content is HTML, fetch content from file in a `content` key
        foreach ($data['data'] as $item) {
            if (strtolower($item['type']) === 'html') {
                $item['content'] = file_get_contents($item['link']) || $item['link'];
            }
        }

        // Set content
        $response->setContent($this->renderView('FrontBundle:AdsManager:index.html.twig', [
            'allowed_type' => $this->container->getParameter('ads')['allowed_types'],
            'content' => $data,
            'id' => $id
        ]));

        return $response;
    }
}

private function getData($id)
{
    // Get path to Ads configuration
    $url = $this->container->getParameter('ads')['uri'];
    // Instanciate a new Parser
    $parser = new Parser();

    // Read configuration and store it in `$data` or throw if we cannot parse it
    try {
        $data = $parser->parse(file_get_contents($url));
    } catch (ParseException $e) {
        throw new ParseException('Unable to parse the YAML string:' . $e->getMessage());
    }

    // If `$id` exists in data, fetch content or throw if it's not found
    try {
        return $data = $data[$id];
    } catch (\Exception $e) {
        throw new \Exception('Cannot find `' + $id + '` id in configuration:' . $e->getMessage());
    }
}

private function randomItem($array) {
    $weights = array_column($array, 'weight');
    $total   = array_sum($weights);
    $random  = rand(1, $total);
    $sum     = 0;

    foreach ($weights as $index => $weight) {
        $sum += $weight;

        if ($random <= $sum) {
            return $array[$index];
        }
    }
}

?>

I know opinions are split between avoiding private methods in controllers and exploding long actions into smaller chunks of code. I went for the latter, but feel free to correct me if you feel like it’s a mistake. I’m no PHP developer. ;)

The View

At this point, our controller is done. We only have to deal with the view. We have a little bit of logic in the templates but it actually makes sense since it’s strictly about how to display the content, so that shouldn’t be too much.

The main idea is: either the data key from content contains several items, in which case we output a slider (Bootstrap carousel in our case), or it has a single item so we output only one. In either case, we don’t output an item directly; we include a partial that deals with type checking in case something is wrong, and redirects to the appropriate partial. But let’s start at the beginning.

{# If there are several items to display #}
{% if content.data|length > 1 %}
  {# Output a carousel #}
  <div class="carousel  slide" data-ride="carousel" data-interval="3000">
    {# Carousel indicators #}
    {% for i in 0..(content.data|length)-1 %}
    {% if loop.first %}
    <ol class="carousel-indicators">
    {% endif %}
      <li data-target=".carousel" data-slide-to="{{ i }}" {% if loop.first %}class="active"{% endif %}></li>
    {% if loop.last %}
    </ol>
    {% endif %}
    {% endfor %}

    {# Carousel items #}
    {% for item in content.data %}
    {% if loop.first %}
    <div class="carousel-inner">
    {% endif %}
      <div class="item{% if loop.first %}  active{% endif %}">
        {% include '@Front/AdsManager/_type.html.twig' with {
          type: item.type, 
          item: item
        } %}
      </div>
    {% if loop.last %}
    </div>
    {% endif %}
    {% endfor %}
  </div>

{# If there is a single item, include it #}
{% else %}

  {% include '@Front/AdsManager/_type.html.twig' with {
    type: (content.data|first).type, 
    item: (content.data|first)
  } %}

{% endif %}

Let’s see what the _type partial looks like:

{# If type is allowed, include the relevant partial #}
{% if type|lower in allowed_type %}
  {% include '@Front/AdsManager/_' ~ type ~ '.html.twig' with { item: item } %}
{# Else print an error #}
{% else %}
  <p>Unknown type <code>{{ type }}</code> for id <code>{{ id }}</code>.</p>
{% endif %}

Last, but not least, our partials for specific types:

{# _image.html.twig #}
<div class="epub">
  <a href="{{ item.target|default('#') }}" class="epub__link">
    <img src="{{ item.link|default('http://cdn.domain.tld/path/to/default.png') }}" 
         alt="{{ item.description|default('Default description') }}" 
         class="epub__image" />
  </a>
</div>
{# _video.html.twig #}
{% if item.link %}
<div class="video-wrapper">
  <iframe src="{{ item.link }}" frameborder="0" allowfullscreen></iframe>
</div>
{% endif %}
{# _html.html.twig #}
{{ item.content|default('') }}

Final thoughts

That’s it! Wasn’t that hard in the end, was it? Yet, it is both a simple and powerful way to manage ads when you cannot rely on third-party services. There is always room for improvement, so feel free to suggest updates and tweaks.

Cheers!

Frequently Asked Questions (FAQs) about Building an Ad Manager with Symfony 2

How can I install the FileManagerBundle in Symfony 2?

To install the FileManagerBundle in Symfony 2, you need to use Composer, a tool for dependency management in PHP. You can install it by running the command composer require artgris/filemanager-bundle. After the installation, enable the bundle by adding it to the list of registered bundles in the app/AppKernel.php file of your Symfony project.

What are the main features of Symfony 2 that make it suitable for building an Ad Manager?

Symfony 2 is a high-performance PHP framework that provides a robust structure for building complex applications like an Ad Manager. It offers features like reusable components, a unified API, and a flexible architecture that allows for full configuration. It also supports database engines like MySQL, PostgreSQL, SQLite, and any other database engine supported by Doctrine.

How can I contribute to Symfony’s development?

Symfony is an open-source project, and contributions are always welcome. You can contribute by submitting bug reports, proposing new features, updating documentation, or writing code patches. Before contributing, it’s recommended to read the contributing guide available on the Symfony website.

How can I use Symfony’s Filesystem component?

Symfony’s Filesystem component provides basic utilities for the filesystem. It can be used to read, write, and modify files and directories. To use it, you first need to install it via Composer using the command composer require symfony/filesystem. Then, you can use its methods like copy(), mkdir(), touch(), etc., to perform filesystem operations.

What is the Fast Track in Symfony?

The Fast Track is a guide provided by Symfony for building applications. It provides a step-by-step approach to building a Symfony application, from setting up the development environment to deploying the application. It’s a great resource for both beginners and experienced developers.

How can I use the Artgris FileManagerBundle in my Symfony project?

The Artgris FileManagerBundle is a file manager bundle for Symfony. After installing it via Composer, you can configure it in your Symfony project by defining routes, services, and parameters as per your requirements. It provides features like file upload, file deletion, directory creation, and more.

How can I handle errors in Symfony 2?

Symfony 2 provides a built-in error handling mechanism. You can create custom error pages for different HTTP status codes. You can also use the Debug component to get detailed error messages during the development phase.

How can I optimize the performance of my Symfony 2 application?

Symfony 2 provides several tools and techniques to optimize the performance of your application. These include using the built-in profiler to identify performance bottlenecks, optimizing the autoloader, using the APC cache, and more.

How can I secure my Symfony 2 application?

Symfony 2 provides a security component that provides a complete security system for your application. It provides features like authentication, authorization, encryption, and more. You can configure it as per your requirements to secure your application.

How can I test my Symfony 2 application?

Symfony 2 provides a testing component that allows you to write unit tests and functional tests for your application. It integrates with PHPUnit and provides a browser-kit and dom-crawler component for web testing.

Kitty GiraudelKitty Giraudel
View Author

Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/they.

advertisingBrunoSsymfonysymfony2yaml
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week