Только это не повредит — я не собираюсь писать о 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
.
И есть такой блок для каждого рекламного места на нашем сайте, так что в основном их дюжина.
Контроллер
Контроллер очень прост: у него одно действие. Сценарий таков:
- получить файл конфигурации;
- разбери это;
- найти данные по заданному идентификатору;
- установить конфигурацию кеша;
- уменьшить до одного элемента, если он
random
; - сделать вид.
<?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('') }}
Последние мысли
Это оно! Разве это не сложно в конце концов? Тем не менее, это простой и эффективный способ управления рекламой, когда вы не можете полагаться на сторонние сервисы. Всегда есть возможности для улучшений, поэтому не стесняйтесь предлагать обновления и настройки.
Ура!