Symfony — комбинируем GridFS файлы с ORM сущностями

в 14:33, , рубрики: Doctrine ORM, mongodb, php, symfony

В предыдущей статье я писал о загрузке файлов в GridFS. Там мы создали MongoDB документ со свойством $file, аннотированным как @MongoDBFile. Так как ORM сущности я использую чаще, чем ODM документы, я искал простой способ получить доступ к документу из сущности.

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

Создадим класс собственного типа поля.

Начнем с создания класса UploadType который определяет столбец типа upload:

namespace DennisUploadBundleTypes;

use DoctrineDBALTypesType;
use DoctrineDBALPlatformsAbstractPlatform;

class UploadType extends Type
{
    const UPLOAD = 'upload';

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getClobTypeDeclarationSQL($fieldDeclaration);
    }

    public function getName()
    {
        return self::UPLOAD;
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}

Чтобы запрашивать ссылку на документ Upload, нам понадобится Doctrine ODM DocumentManager для создания такой ссылки, для этого добавим сеттер.

use DoctrineODMMongoDBDocumentManager;

// ...

private $dm;

public function setDocumentManager(DocumentManager $dm)
{
    $this->dm = $dm;
}

Чтобы убедиться что в базу сохраняется только id документа Upload, мы переопределим метод convertToDatabaseValue, чтобы он возвращал только id документа.

use DennisUploadBundleDocumentUpload;
use DoctrineDBALTypesConversionException;

// ...

public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
    if (empty($value)) {
        return null;
    }

    if ($value instanceof Upload) {
        return $value->getId();
    }

    throw ConversionException::conversionFailed($value, self::UPLOAD);
}

Для возвращения ссылки на документ Upload, после того как сущность получила его из базы, мы переопределяем метод convertToPHPValue. Как видно ниже, создание ссылки — это просто передача класса и id документа в метод getReference() класса DocumentManager. Т.к. в методе convertToDatabaseValue мы решили возвращать id документа Upload, то можно сразу его использовать.

// ...

public function convertToPHPValue($value, AbstractPlatform $platform)
{
    if (empty($value)) {
        return null;
    }

    return $this->dm->getReference('DennisUploadBundleDocumentUpload', $value);
}

Стоит отметить, что большое преимущество создания ссылки на документ, вместо использования репозитория DennisUploadBundle:Upload для извлечения документа заключается в том, что документ будет получен из базы и инициализирован только тогда, когда будет запрошено поле с этим документом. Когда вы используете репозиторий DennisUploadBundle:Upload для поиска документа и присваивания его к свойству сущности, то экземпляр документа будет создаваться для каждой сущности Image, которую вернет ORM EntityManager. То есть на 100 сущностей будет создано такое же количество документов, что, конечно же, неэффективно. Создание ссылки дает гарантию, что документ будет создан только тогда, когда вы его запросите.

Регистрация собственного типа поля.

Теперь, когда наш UploadType у меет правильно конвертировать документ Upload, пора его использовать в нашем Symfony приложении. Согласно этой статье Matthias Noback, лучшее место для добавления нового типа в Доктрину — конструктор бандла, где затем будет подгружены ODM DocumentManager зависимости в наш UploadType

namespace DennisUploadBundle;

use SymfonyComponentHttpKernelBundleBundle;
use DoctrineDBALTypesType;

class DennisUploadBundle extends Bundle
{
    public function __construct()
    {
        if (!Type::hasType('upload')) {
            Type::addType('upload', 'DennisUploadBundleTypesUploadType');
        }
    }

    public function boot()
    {
        $dm = $this->container->get('doctrine.odm.mongodb.document_manager');

        /* @var $type DennisUploadBundleTypesUploadType */
        $type = Type::getType('upload');

        $type->setDocumentManager($dm);
    }
}

Использование UploadType

Для демонстрации использования нашего нового поля upload, начну с создания сущности Image:

namespace AcmeDemoBundleEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class Image
{
    /**
     * @ORMId
     * @ORMColumn(type="integer")
     * @ORMGeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORMColumn(type="string", length=100)
     */
    protected $name;

    /**
     * @ORMColumn(type="upload")
     */
    protected $image;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setImage($image)
    {
        $this->image = $image;
    }

    public function getImage()
    {
        return $this->image;
    }
}

Если взглянуть на аннотацию @ORMColumn поля $image, то видно, что в параметр type достаточно добавить имя нашего типа UploadType («upload») и Доктрина будет использовать объект класса UploadType при чтении или записи в базу поля $image.

Обработка формы

Обработка формы, которая была создана для нашей сущности Image примерно такая же, как и любая форма, основанная на сущности. Единственное дополнение заключается в том, что вы должны убедиться, что загруженный файл сохранился в GridFS и созданный документ Upload присвоен полю $image сущности Image.

namespace AcmeDemoBundleController;

use DennisUploadBundleDocumentUpload;

class ImageController extends Controller
{
    public function newAction(Request $request)
    {
        // ...

        $form->bind($request);

        if ($form->isValid()) {
            /** @var $upload SymfonyComponentHttpFoundationFileUploadedFile */
            $upload = $image->getImage();

            $document = new Upload();
            $document->setFile($upload->getPathname());
            $document->setFilename($upload->getClientOriginalName());
            $document->setMimeType($upload->getClientMimeType());

            $dm = $this->get('doctrine.odm.mongodb.document_manager');
            $dm->persist($document);
            $dm->flush();

            $image->setImage($document);

            $em = $this->getDoctrine()->getManager();
            $em->persist($image);
            $em->flush();
        }
    }
}

Если после успешного сохранения формы вы посмотрите в таблицу image, вы увидите, что была создана запись, в которой в поле image записан id документа Upload.

Извлечение изображения из базы.

Описанный ниже метод почти такой же, как метод showAction контроллера UploadController из предыдущей статьи. Единственное отличие — в том, что вы можете использовать репозиторий AcmeDemoBundle:Image для извлечения сущности Image, а затем извлечь документ Upload простым вызовом getImage(). Повторюсь, документ Upload будет извлечен из MongoDB и создан только при вызове метода getImage().

/**
 * @Route("/{id}", name="image_show")
 */
public function showAction($id)
{
    $image = $this->getDoctrine()->getManager()
        ->getRepository('AcmeDemoBundle:Image')
        ->find($id);

    $response = new Response();
    $response->headers->set('Content-Type', $image->getImage()->getMimeType());

    $response->setContent($image->getImage()->getFile()->getBytes());

    return $response;
}

На этом все! Теперь у нас есть собственный UploadType, который обрабатывает ссылки на документы Upload нашей сущности Image. Я убежден, что такой подход к созданию собственного класса Type обеспечивает легкий способ комбинирования ODM документов и ORM сущностей.

Единственный огромный недостаток — это то, что вам вручную нужно сохранять (persist()) документ перед сохранением сущности. Это точно не то, что хотелось бы повторять в каждом подобном контроллере с таким комбинированием. В своей следующей статье я попытаюсь побороть эту проблему.

Автор: Reshat

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js