Pull to refresh

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

PHP *Symfony *API *
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;
    }
}




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

Конец


На самом деле, подобных советов еще очень много и если вам будет интересно, я с радостью ими поделюсь.
Tags:
Hubs:
Total votes 22: ↑18 and ↓4 +14
Views 16K
Comments Comments 40