Статьи

Создание менеджера рекламы в Symfony 2

Только это не повредит — я не собираюсь писать о Sass, а о Symfony. Мне пришлось немного поработать с бэкэндом на работе, и в итоге мне пришлось решить интересную проблему, включающую довольно много вещей, поэтому я подумал, что написать статью об этом было бы неплохо.

Но сначала позвольте мне объяснить. Основная идея заключалась в том, чтобы построить рекламный менеджер. Что, черт возьми, менеджер по рекламе вы говорите? Допустим, на вашем сайте / в приложении есть несколько мест для показа рекламы. У нас есть такие вещи на нашем сайте, и одна из наших команд (частично) посвящена тому, чтобы оживить эти места с помощью контента.

Теперь по некоторым скучным причинам я не буду перечислять здесь, мы не могли использовать существующий инструмент, поэтому мы были обречены на создание чего-то с нуля. Как обычно, мы хотели многое сделать без большого количества кода, сохраняя при этом простоту для конечного пользователя (который не является разработчиком). Я думаю, что мы придумали довольно приличное решение для нашего маленького проекта.

Вот функции, которые мы настроили:

  • Настройка YAML + доступ по FTP;
  • Либо изображения, видео или HTML-контент;
  • Возможность настройки длительности кеша;
  • Ползунки (да, шаблон отстой) или случайный элемент в коллекции.

Идея довольно проста. В шаблонах Twig мы используем render_esi (подробнее здесь ), нажимая на действие контроллера, передавая ему уникальный ключ (в основном имя рекламного места), например:

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

Затем действие выбирает файл конфигурации YAML, извлекает данные, связанные с данным идентификатором, и отображает шаблон. На этом этапе шаблон выполняет очень простую логику в зависимости от того, к какому типу контента относятся данные элементы (изображения, видео, HTML…).

Готов? Пошли.

Глобальная конфигурация

Есть две вещи, которые мы должны иметь в глобальной конфигурации (вероятно, файл parameters.yml ): путь к файлу конфигурации ( ads.yml ) и массив разрешенных типов мультимедиа.

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

Файл конфигурации

Файл конфигурации поддерживается командой, отвечающей за создание рекламы. YAML — это дружественный человеку язык, который идеально подходит для простой конфигурации.

Этот файл построен так:

 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 

Где:

  • cache_public определяет, должен ли кеш быть публичным или приватным;
  • cache_shared_max_age определяет максимальный возраст для кэша в сети сервера;
  • cache_max_age определяет максимальный cache_max_age кэша в браузере клиента;
  • random определяет, следует ли рассматривать точку как слайдер (несколько элементов, идущих один за другим) или статический элемент, выбранный случайным образом;
  • data — это массив элементов, все из которых отображаются, если для параметра random установлено значение false или сводятся к одному элементу, если значение random равно true.

Каждый элемент data является объектом, состоящим из:

  • type определяет type мультимедиа: image , video или html ;
  • link определяет медиа-контент, то есть абсолютный URL-адрес файла изображения, видео-службы или файла HTML;
  • target определяет цель ссылки за изображением, если type является image , в противном случае игнорируется;
  • weight определяет увеличение, когда data содержат несколько элементов, а для параметра random установлено значение true .

И есть такой блок для каждого рекламного места на нашем сайте, так что в основном их дюжина.

Контроллер

Контроллер очень прост: у него одно действие. Сценарий таков:

  1. получить файл конфигурации;
  2. разбери это;
  3. найти данные по заданному идентификатору;
  4. установить конфигурацию кеша;
  5. уменьшить до одного элемента, если он random ;
  6. сделать вид.
 <?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]; } } } ?> 

Я знаю, что мнения разделились между отказом от приватных методов в контроллерах и разбиением длинных действий на меньшие куски кода. Я пошел за последним, но не стесняйтесь поправлять меня, если вы чувствуете, что это ошибка. Я не разработчик PHP. 😉

Вид

На данный момент наш контроллер готов. Нам остается только разобраться с видом. У нас есть немного логики в шаблонах, но на самом деле это имеет смысл, поскольку речь идет строго о том, как отображать контент, так что это не должно быть слишком много.

Основная идея такова: либо ключ data из content содержит несколько элементов, в этом случае мы выводим ползунок (в нашем случае карусель Bootstrap), либо он содержит один элемент, поэтому мы выводим только один. В любом случае мы не выводим элемент напрямую; мы включаем частичное, которое имеет дело с проверкой типа в случае, если что-то не так, и перенаправляет на соответствующий частичный. Но давайте начнем с самого начала.

 {# 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 %} 

Давайте посмотрим, как _type частичный _type :

 {# 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 %} 

И последнее, но не менее важное, наши частичные функции для определенных типов:

 {# _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('') }} 

Последние мысли

Это оно! Разве это не сложно в конце концов? Тем не менее, это простой и эффективный способ управления рекламой, когда вы не можете полагаться на сторонние сервисы. Всегда есть возможности для улучшений, поэтому не стесняйтесь предлагать обновления и настройки.

Ура!