Особенности разработки API на Symfony2

  • Tutorial
Так вышло, что всю свою недолгую карьеру я занимаюсь разработкой API для мобильных приложений и сайтов на Symfony2. Каждый раз открываю для себя все новые знания, которые кому-то покажутся очевидными, а кому-то помогут сэкономить не мало времени. Об этих знаниях и пойдет речь.

Формы


Вообще использовать дефолтные формы для API не лучшая идея, но если вы все же решились, то вам необходимо не забывать о некоторых особенностях. Изначально формы в symfony делались для обычных сайтов, где фронтенд и бекенд объединены.

Первая проблема возникает с entity type. Когда вы отсылаете запрос к методу, который использует entity type в формах – сначала достаются все сущности указанного класса, и только потом запрос на получение нужной сущности по отправленному id. Многие не знают об этом и очень удивляются, почему метод работает так долго.

Пример решения
EntityType.php
<?php

namespace App\CommonBundle\Form\Type;

use App\CommonBundle\Form\DataTransformer\EntityDataTransformer;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class EntityType extends AbstractType
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults([
            'field' => 'id',
            'class' => null,
            'compound' => false
        ]);

        $resolver->setRequired([
            'class',
        ]);
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addModelTransformer(new EntityDataTransformer($this->em, $options['class'], $options['field']));
    }

    public function getName()
    {
        return 'entity';
    }
}


EntityDataTransformer.php
<?php

namespace App\CommonBundle\Form\DataTransformer;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;

class EntityDataTransformer implements DataTransformerInterface
{
    private $em;
    private $entityName;
    private $fieldName;

    public function __construct(EntityManager $em, $entityName, $fieldName)
    {
        $this->em = $em;
        $this->entityName = $entityName;
        $this->fieldName = $fieldName;
    }

    public function transform($value)
    {
        return null;
    }

    public function reverseTransform($value)
    {
        if (!$value) {
            return null;
        }

        return $this->em->getRepository($this->entityName)->findOneBy([$this->fieldName => $value]);
    }
}


services.yml
    common.form.type.entity:
        class: App\CommonBundle\Form\Type\EntityType
        arguments: [@doctrine.orm.entity_manager]
        tags:
            - { name: form.type, alias: entity }



Вторая проблема возникает с checkbox type, который пытаются использовать для булевых значений, но особенность работы этого типа такова, что если ключ существует и он не пустой, то вернется true.

Пример решения
BooleanType.php
<?php

namespace App\CommonBundle\Form\Type;

use App\CommonBundle\Form\DataTransformer\BooleanDataTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class BooleanType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addViewTransformer(new BooleanDataTransformer());
    }

    public function getParent()
    {
        return 'text';
    }

    public function getName()
    {
        return 'boolean';
    }
}


BooleanDataTransformer.php
<?php

namespace App\CommonBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class BooleanDataTransformer implements DataTransformerInterface
{
    public function transform($value)
    {
        return null;
    }

    public function reverseTransform($value)
    {
        if ($value === "false" || $value === "0" || $value === "" || $value === 0) {
            return false;
        }

        return true;
    }
}


services.yml
    common.form.type.boolean:
        class: App\CommonBundle\Form\Type\BooleanType
        tags:
            - { name: form.type, alias: boolean }



JMS Serializer


Во всех статьях про создание API советуется именно это замечательное расширение. Люди смотрят простенький пример, где у сущностей есть две serialization groups: details и list, и начинают у каждой сущности использовать именно эти названия и все замечательно работает, пока не попадется какая-нибудь связанная сущность, у которой группы названы точно так же и выводится очень много лишней, не нужной информации. Также это может уводить в бесконечный цикл при сериализации, если обе модели выводят связь друг с другом.

Пример неправильного использования
News.php
<?php

use JMS\Serializer\Annotation as Serialization;

class News
{
    /**
     * @Serialization\Groups({"details", "list"})
     */
    protected $id;

    /**
     * @Serialization\Groups({"details", "list"})
     */
    protected $title;

    /**
     * @Serialization\Groups({"details", "list"})
     */
    protected $text;

    /**
     * Связь с сущностью User
     *
     * @Serialization\Groups({"details", "list"})
     */
    protected $author;
}


User.php
<?php

use JMS\Serializer\Annotation as Serialization;

class User
{
    /**
     * @Serialization\Groups({"details", "list"})
     */
    protected $id;

    /**
     * @Serialization\Groups({"details", "list"})
     */
    protected $name;

    /** Огромный список полей отмеченных группами list и details */
}


NewsController.php
<?php

class NewsController extends BaseController
{
    /**
     * @SerializationGroups({"details"})
     * @Route("/news/{id}", requirements={"id": "\d+"})
     */
    public function detailsAction(Common\Entity\News $entity)
    {
        return $entity;
    }
}




В примере видно, что при получении новости в поле author будут все поля, которые в User с группой details, что явно не входит в наши планы. Казалось бы, очевидно, что так делать нельзя, но, к моему удивлению, так делают многие.

Я советую именовать группы как %entity_name%_details, %entity_name%_list и %entity_name%_embed. Последняя нужна как раз для тех случаев, когда есть связанные сущности и мы хотим вывести какую-то связанную сущность в списке.

Пример правильного использования
News.php
<?php

use JMS\Serializer\Annotation as Serialization;

class News
{
    /**
     * @Serialization\Groups({"news_details", "news_list"})
     */
    protected $id;

    /**
     * @Serialization\Groups({"news_details", "news_list"})
     */
    protected $title;

    /**
     * @Serialization\Groups({"news_details", "news_list"})
     */
    protected $text;

    /**
     * Связь с сущностью User
     *
     * @Serialization\Groups({"news_details", "news_list"})
     */
    protected $author;
}


User.php
<?php

use JMS\Serializer\Annotation as Serialization;

class User
{
    /**
     * @Serialization\Groups({"user_details", "user_list", "user_embed"})
     */
    protected $id;

    /**
     * @Serialization\Groups({"user_details", "user_list", "user_embed"})
     */
    protected $name;

    /** Огромный список полей, которые отмечены группами user_list и user_details */
}


NewsController.php
<?php

class NewsController extends BaseController
{
    /**
     * @SerializationGroups({"news_details", "user_embed"})
     * @Route("/news/{id}", requirements={"id": "\d+"})
     */
    public function detailsAction(Common\Entity\News $entity)
    {
        return $entity;
    }
}




При таком подходе будут только необходимые поля, к тому же это можно будет использовать в других местах, где тоже нужно вывести краткую информацию о пользователе.

Конец


На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 40

    +2
    Когда открывал статью то думал что первым же пунктом увижу что-то по поводу форм. Обрадовался когда увидел хотя бы упоминания что формы юзать в контексте API не ок (да, когда-то я думал что ок).

    JMS Serializer — самое распространенное решение не лишенное проблем. По сути оно навязывает решение проблем, которое само и привносит.

    Далее я надеялся увидеть альтернативные подходы… но увы и ах, конец… на самом интересном месте.

    Я все же думаю что стоит выкинуть JmsSerializer и выкидывать простенькие DTO из и в сервисный слой. Хоть в виде ассоциативных массивов, хоть в виде классов с сеттерами, хоть в виде stdObject.

    Я бы хотел увидеть, скажем… отложенное формирование view во фронт контроллере, что бы можно было флаш доктрины туда же вынести.
      0
      А можно пример какой-нибудь, как вы работаете без JMS Serializer?

      Насчет форм:
      У нас все запросы, которые содержат в себе какие-либо параметры, обязательно мапятся на модели. Даже если это какой-нибудь список с фильтрами. Поэтому мы отказались полностью от форм и просто в моделях пишем что-то подобное:
      class IntakeFilter
      {
          protected $user;
      
          /**
           * @RequestMapper(type="integer")
           */
          protected $limit = 20;
      
          /**
           * @RequestMapper(type="integer")
           */
          protected $offset = 0;
      
          /**
           * @RequestMapper(type="boolean")
           */
          protected $isCompleted;
      
          /**
           * @RequestMapper(type="string")
           * @Assert\Length(min=2)
           */
          protected $query;
      }
      


      И в контроллере:
      public function actionList() 
      {
          $model = $this->handleRequest(new App\Model\IntakeFilter($this->getUser());
      
          return $repository->findByFilter($model);
      }
      
        +1
        Ну не сказать что я отказался от JmsSerializer, только на домашних проектах пока что. Но как-то так:

        class SomeEntity {
            public function getShortInfoView() {
                
                // думаю эту штуку можно сократить для пущей читабельности. 
                // А если у нас будет много вьюшек, можно DRY-ить эти вещи
                // или если дела будут совсем плохи, делать нормальные объекты DTO.
                return [
                     'id' => $this->id,
                     'name' => $this->name,
                     'some_info' => $this->someInfo,
                     'connected_entities' => $this->connectedEntities->map(function (SomeConntectedEntity $entity) {
                          return $entity->getShortInfoView();
                     })->toArray();
                ];
            }
        }
        
        class SomeController {
        
            public function addSomeAction(Request $request) { 
                 // обычно это происходит во фронт контроллере для всех запросов с Content-type application/json
                 // тут просто для наглядности
                 $dto = json_encode($request->getContent());
        
                 // у меня умерла под конец дня фантазия, так что простите за именования в духе some creator
                 $shortInfoView = $this->get('app.some_creator')->createSome($dto);
         
                 // это так же происходит во фронт контроллере но мне лень
                 // что до ситуация с ID и их получением до flush
                 // я использую postgresql и тамошние последовательности, которые прекрасно это дело разруливают 
                 // а автоинкремент mysql это зло
                 $this->get('doctrine.orm.entity_manager')->flush();
        
                 // обычно из контроллера я возвращаю только данные или какой-то View объект в духе FostRest
                 return new JsonResponse($shortInfoView);
            }
        }
        
        class SomeCreator 
        {
             private $someRepository;
        
             public function __construct(SomeRepository $repository) {
                  $this->someRepository = $repository;
             }
        
             public function create($dto) {
             
                 // тут можно провалидировать $dto но лень
                 // всеравто чуть что конструктор энтити бросит исключение
                 // ну и лучше что бы $dto был \stdObject. 
                 // Тогда потом можно будет в случае усложнения логики 
                 // переделать его на нормальный DTO объект со своим типом
                 $entity = new SomeEntity($dto['name'], $dto['short_info']);
        
                 $this->someRepository->add($entity);
                 
                 return $entity->getShortInfoView();
             }
        }
        


        вот как-то так. Заметте что внутри сервисного слоя заключена все знание о том как наша бизнес логика работает и ни одного упоминания о доктрине. Так же, поскольку все энтити крутятся в unit-of-work, а коммит транзакции происходит вне оного, не очень безопасно возвращать в контроллер саму энтити, так как можно случайно там поменять состояние оной. Лучше плюнуть наружу DTO, что мы и делаем. А уж с простым массивом справится и json_encode.

        Как-то так. Пока вариантов лучше я не придумал и не знаю…
          0
          Ммм… опечатался слегка… вместо
          $entity = new SomeEntity($dto['name'], $dto['short_info']);
          

          надо
          $entity = new SomeEntity($dto['name'], $dto['some_info']);
          
            0
            Интересный вариант, схема правда та же самая, но инструменты другие. Мне нравится, что это работает довольно быстро, ибо выбрасываем jms serializaer, но больше плюсов, честно говоря, не вижу :(
              0
              Как же не видите? Все явно, можно полностью проследить всю логику от момента получения данных запроса до вывода наружу. И все это из сервисного слоя приложения. Полностью вся бизнес логика по одной фиче в одном месте. И все можно покрыть тестами. Как по мне это намного более весомый аргумент чем производительность.
                0
                А ну и еще, я указывал это в комментах в коде но может вы не обратили внимание. У меня обычно flush выполняется непосредственно перед отправкой ответа, во фронт контроллере. И если мы выплюнем сущность из сервиса в контроллер, кто-то может изменить (случайно или специально) состояние сущности и мы получим баги. При моем варианте же возвращается DTO, и если мы чего поменяем в нем, то как бы и пофигу.
            0
            И еще очень интересно, о каких именно проблемах с JMS Serializer вы говорите, ибо пока что это очень удобно. Контроллеры выглядят примерно так:
                /**
                 * @Common\Annotation\SerializationGroup("clinic_details")
                 * @Common\Annotation\HttpStatus(201)
                 *
                 * @Method("POST")
                 * @Route
                 */
                public function addAction()
                {
                    $entity = $this->handleRequest(new Common\Entity\Clinic($this->getUser());
                    $this->persist($entity, true);
            
                    return $entity;
                }
            
              0
              группы сериализации. Это очень мощный инструмент но он вносит в умы людей много смуты. Они начинают оперировать целыми сущностями в рамках какой-то бизнес логики, а не частью оной. То есть по сути необходимость в них есть только за счет того что никто не хочет писать скучный код формирующий DTO или вообще не понимает зачем это нужно и в чем крутость. У меня вот и symfony/validation не используется). Ну а для сложной иерархии состояний можно подключить еще и Value Object-ы, и разруливать это дело на уровне контроллера. Код можно DRY-ить, он явный, можно посадить любого человека знающего PHP и он сразу сможет писать бизнес логику. Ну и все можно покрыть юнит тестами, со штуками типа JMSSerializer, FosRest и т.д. спасают только интеграционные и функциональные тесты.
                0
                Мне кажется у JMSSerializer слишком много плюшек, чтобы от него отказываться, а проблему с сериализационными группами надо решать не выделением имен по каким-то действиям (list, details), а используя предметную область (default, secure).
                  0
                  Мы у себя сделали _fields парметр, как во всех API. Оно ни в коем случае не заменяет группы, которые мы используем чтобы разделить рендеринг приватной информации от общей.
                    0
                    Проблемы с выделением групп вообще нет, просто JmsSerializer позволяет тебе выплюнуть наружу целиком твои сущности и дальше разруливать сериализацию гурппами, вместо того что бы выплюнуть DTO где есть все что нужно и ничего лишнего. Оно конечно удобно, позволяет тебе не писать туповатый бойлерплейт, но многие слишком много на него вешают. Доходит до того что весь контроллер состоит из десериализации и персиста, а вся логика размазана по хэндлерам и т.д
                0
                Можно вкратце объяснить почему формы в контексте API лучше не использовать? Это же просто меппер реквеста на объект. Медленно?
                  0
                  Не медленно, у вас нету формы, потому использовать Symfony/forms вне форм не целесообразно. Обычно их используют только из-за хорошей интеграции с доктриной из коробки.
                    0
                    эээ что вы подразумеваете под
                    хорошей интеграции с доктриной из коробки
                    ?
                      0
                      Угадывание типов на основе доступной информации о мэппинге подтянутой из доктрины. Я вот о чем.
                        0
                        да ну, это фича дополнительного характера. Для меня в формах в разрезе API то что я могу описать правила на получаемые данные, провалидировать их и замапить на сущность или дто.
                    0
                    Причин много, во первых в API нет форм, как уже говорили, во вторых, для того, чтобы формы начали более менее работать – нужно множество костылей, которых накапливается такое количество, что в какой-то момент задаешься вопросом «Ну и нафига все это?», в третьих это действительно очень медлено
                      0
                      что для вас формы?
                        +1
                        Обычные html формы, у которых другие типы данных, есть отображение, данные берутся из $_GET/$_POST
                          0
                          ну вас никто не заставляет их рендерить в html. Я не вижу особых проблем почему для API не стоит использовать формы. Они хорошо подходят для маппинг, валидация реквест данных. Конечно в какой-то момент они избыточны, но согласитесь, что писать велосипед дороже. Хотя есть еще вариант использовать OptionsResolver, но не использовал, не могу по нему сказать какие плюсы или минусы.
                            0
                            вот нашел линку тыц в принципе выглядит неплохо. надо попробовать
                              0
                              Ок, допустим мы выбрали формы. И флоу будет таким:
                              — Приняли запрос
                              — Во фронт контроллере сделали json_encode тела запроса
                              — Забиндили результат работы json_encode на энтити через формы
                              — Валидируем состояние нашей сущности
                              — забираем из формы готовую сущность, куда формы запихали все через сеттеры. (ненавижу тупые сеттеры в сущностях)

                              А как можно:
                              — Приняли запрос
                              — Во фронт контроллере сделали десериализацию напрямую в сущность через JmsSerializer тела запроса, При этом JmsSerializer достаточно гибок, и имеет меньше ограничений.
                              — Валидируем состояние нашей сущности
                              — Как бы все, можно это делать в ParamConverter, и тогда в контроллер придет уже все готовое

                              (хоть я и не одобряю такой подход)

                              Вообще если вы берете формы, то работать напрямую с сущностью не очень хорошая идея. Да, на маленьких проектах норм, но чем сложнее проект тем больше боли они приносят. Если работать с DTO то и боли меньше с формами, и сущности красивые выходят.
                                0
                                (ненавижу тупые сеттеры в сущностях)

                                не вы один

                                Ну а если помимо API у вас есть веб сайт, где есть такие же сущности, то тогда легче юзать форму для двух вариантов.

                                Основной мой посыл в том что формы это не панацея. Все зависит от разработчика, который должен понимать что делает, а то потом может быть очень плохо)
                    +1
                    По поводу entity type. Все очень просто, вы можете указать в опциях query builder и тогда он будет вытягивать с условием in. Да не очевидный кейс но он работает
                      +1
                      Я бы в контексте Symfony + API добавил бы в статью ещё и рассказ об FOSRestBundle, о какой-нибудь JWT аутентификации, обновлении и удалении токена и прочем. Ибо когда мне пришлось выполнять данную задачу, то столкнулся с кучей проблем уже на первых парах.

                      Было бы интересно почитать :)
                        +2
                        JWT аутентификации,

                        зачем он вам? лучше взять FOSOAuthServerBundle и проблем не будет.
                          +1
                          Вот видите, об этом я и говорю — информации в статье крайне мало. Мне, как минимум, было бы интересно почитать и перенять опыт :)

                          За бандл отдельное спасибо ;)
                            +1
                            Согласен с тем что информации мало, в первую очередь хотел написать статью, для того, чтобы понять насколько это интересно другим. Опыта не мало, особенно в каких-то мелочах, но со временем это все кажется очевидным и просто не понимаешь о чем писать.

                            У нас на проектах, так исторически сложилось, что мы не используем бандлы для авторизации. Каждый запрос подписывается с помощью заголовка X-Access-Token, а в случае успеха юзер подставляется в security.context. Все это делается буквально в 1 listener, и мне до конца не понятно, зачем использовать что-то готовое, когда написать код дело 3 минут.

                            Если интересно, могу поделиться кодом или еще лучше написать об этом в следующей статье :)
                          0
                          Многие считают OAuth2 избыточным для REST API. Мне этот вариант лично нравится, но не могли бы вы как-то аргументировать почему не будет проблем и почему это круто? Ну мол… можно использовать старую добрую digest авторизацию которая есть в symfony из коробки.
                            +1
                            Я бы не сказал что OAuth2 избыточный. Все зависит от того для чего его применять. Если вы пишите апи который используете только вы, то да возможно нет смысла тянуть туда OAuth2. digest и oauth разные цели преследую. oauth в первую очередь для того чтоб авторизировать приложение.
                          +1
                          Про FOSRestBundle, как и про многие бандлы, которые советуют использовать при создании API, я могу сказать только одно: их очень удобно использовать первое время, пока проект очень маленький и простой. Потом начинаются проблемы, из-за того, что разработчик не понимает как это работает и начинает это использовать не как это изначально, возникают проблемы и на их решение уходит большая часть времени. Но как только схема работы становится очевидной, приходит понимание, что можно сделать и лучше, не использовать кучу кода, который будет висеть мертвым грузом.

                          Rest Bundle – это же по сути набор небольших скриптов и библиотек, уложенных в определенную структуру. Мне гораздо удобнее использовать их отдельно. Например: бандл для сериализации использует тот же JMS Serializer, только лишь ограничивая в его кастомизации.
                            0
                            Да, это выбор каждого. Я предпочитаю использовать уже написанное и покрытое тестами, а не писать велосипеды. В некоторых случаях своё, конечно, оправданно, не спорю.
                              0
                              Если будет лишнее время, на покрытие тестами, постараюсь залить куда-нибудь свои наработки. Что-то вроде своего взгляда на то, как должен выглядеть REST Bundle, ориентированный в первую очередь на скорость разработки и гибкость.
                                0
                                Да хоть так залейте, а если будет что-то интересное с покрытием кода тестами можно помочь. Гитхаб, опенсурс, все такое.
                          0
                          Разрешите поделиться нашим опытом:
                          1. JMSSerializer переусложнен и очень медленный. Даже опытные разработчики могли потерять часы, чтобы что-то подправить в API сгенерированном этим сервисом.
                          Если нужно сделать что-то сложное, например поиск, это вообще ад.

                          Джуниора вообще подпускать к этому бандлу нереально.
                          — Выход, сделали свои трансформеры, простые.
                          Код понятен, отлично дебажится и тестируется.
                          Сильно ускорили разработку, поскольку просто (KISS).

                          2. Symfony2 Form Component не используем.
                          Хороший компонент, но когда формы обычные.
                          Если речь идет о REST API, особенно если нужно сделать ~100 entity — формы начинают сильно замедлять разработку. Особенно для PATCH метода.

                          p.s. Вообще сейчас мигрируем на Spring(java).
                          С Symfony достаточно давно (c 2009), много кода написано.
                          Да еще и афилированные партнеры SensioLabs (про это могу в привате отдельно рассказать, кому интересно).
                          Но… java работает примерно от 10 до 200 раз быстрее.

                          Архитектура очень похожа на SF2, работы столько же, а выхлоп в разы круче.
                            0
                            JMSSerializer переусложнен и очень медленный. Даже опытные разработчики могли потерять часы, чтобы что-то подправить в API сгенерированном этим сервисом.

                            Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.

                            Если нужно сделать что-то сложное, например поиск, это вообще ад.

                            А причем serializer к поиску? Если и необходимо дополнительные данные вывести (количество всех записей, текущая страница/сдвиг и т.д.) — ну добавили объект-обертку ResultSet к результирующей коллекции и полет нормальный

                            Джуниора вообще подпускать к этому бандлу нереально.

                            Тут сложно не согласиться.

                            Но… java работает примерно от 10 до 200 раз быстрее.

                            До 200 раз? Приведите, пожалуйста, пример
                              0
                              Приведите, пожалуйста, конкретный пример. К слову, с такой же ситуацией я и сам недавно столкнулся, но количество времени, которое JMSSerializer сэкономил, ставит на нет некоторые недочеты и сложности.

                              Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.

                              До 200 раз? Приведите, пожалуйста, пример

                              www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.

                              p.s. Я холиварить не особенно хочу, просто выбор сделал и делюсь с вами.
                                0
                                Сделайте кастомную выгрузку коллекции, чтобы там было 3-4 связи и это было скажем на хотябы на 100K записей. А также выводились данные о пагинации.


                                Ну вы же не 100К записей выводите. У нас есть пример с многомиллионной таблицей — все работает. Сами связи и степень их вложенности ведь запросто контролируются аннотациями, более того можно что-нибудь сложное выбирать в POST_SERIALIZE джоином, если много запросов не устраивает.

                                Про пагинацию в предыдущем комментарии ответил, нет ничего сложного и «очень медленного».

                                > www.techempower.com/benchmarks — Искать Spring и Symfony2. На самом деле так и есть.

                                Хотелось бы реальный пример, ну да ладно.

                                PS никто не холиварит, здоровый интерес :)

                          Only users with full accounts can post comments. Log in, please.