Create doctrine association on database field with concatenated IDs

时间:2015-06-30 13:58:15

标签: php mysql symfony doctrine-orm

I'm building a Symfony 2 / Doctrine 2 application on top of an existing MySQL database. Due to poor decisions in the past, i'm stuck with references that are concatenated in a table-column. Unfortunately remodeling the database is not an option.

E.g. entity "Product" referring to multiple "Categories":

| id | name      | category_ids |
|----|-----------|--------------|
| 1  | product a | 1,2,5        |
| 2  | product b | 3,4,1        |
| 3  | product c | 2            |

I would like to have the method getCategories available in my "Product" entity which would return a Collection of Category objects.

Is there any way to achieve this with Doctrine?

Maybe use custom code that is based on "FIND_IN_SET"?

SELECT c.* 
  FROM product p 
    LEFT OUTER JOIN category c ON FIND_IN_SET(c.id, p.category_ids)
  WHERE p.id=:product_id;

Or maybe define the association with exploded values?

explode(',',$this->category_ids)

I try to avoid having to use the EntityManager each time i need to retrieve Categories from my Product entity. Because:

  • Injecting the EntityManager in Entities is bad practice
  • Using the EntityManager each time in my controllers a bit against the DRY-principle
  • I have no idea how to achieve this in Symfony FormTypes to have a Choice/Entity field with the relevant Categories for a Product.

1 个答案:

答案 0 :(得分:2)

解决方案1 ​​

您可以为getCategories方法制定水合策略,并在水化器类中注册此策略(甚至可以是DoctrineObject保湿剂)。类似的东西:

<强>策略

<?php

namespace My\Hydrator\Strategy;

use Doctrine\Common\Collections\ArrayCollection;
use My\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;

class CategoriesStrategy implements StrategyInterface, ObjectManagerAwareInterface
{
    /**
     * @var ObjectManager
     */
    protected $objectManager;

    /**
     * @param ObjectManager $objectManager
     * @param String $hostName
     */
    public function __construct(ObjectManager $objectManager)
    {
        $this->objectManager = $objectManager;
    }

    /**
     * @param array $value
     * @return ArrayCollection
     */
    public function extract($value)
    {
        $collection = new ArrayCollection();

        if (is_array($value)) {
            foreach ($value as $id) {
                $category = $this->getObjectManager()->find(Category::class, $id);
                $collection->add($category);
            }
        }

        return $collection;
    }

    /**
     * @param ArrayCollection $value
     * @return array
     */
    public function hydrate($value)
    {
        $array = array();
        /** @var Category $category */
        foreach ($value as $category) {
            $array[] = $category->getId();
        }
        return $array;
    }

    /**
     * @param ObjectManager $objectManager
     * @return $this
     */
    public function setObjectManager(ObjectManager $objectManager)
    {
        $this->objectManager = $objectManager;

        return $this;
    }

    /**
     * @return ObjectManager
     */
    public function getObjectManager()
    {
        return $this->objectManager;
    }
}

您可能需要一家工厂在您的保湿箱内注册您的CategoriesStrategy

保湿工厂

<?php

namespace My\Hydrator;

use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject;
use My\Hydrator\Strategy\CategoriesStrategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceManager;

class MyHydratorFactory implements FactoryInterface
{
    /**
     * @param  ServiceLocatorInterface $serviceLocator
     * @return DoctrineObject
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /** @var ServiceManager $serviceManager */
        $serviceManager = $serviceLocator->getServiceLocator();

        /** @var ObjectManager $objectManager */
        $objectManager = $serviceManager->get('bc_object_manager');

        /** @var DoctrineObject $hydrator */
        $hydrator = new DoctrineObject($objectManager);
        $hydrator->addStrategy('categories', new CategoriesStrategy($objectManager));
        return $hydrator;
    }
}

这未经过测试,但您明白了......

解决方案2

另一种解决方案是为您的类别注册DBAL类型。您可以在the Doctrine2 documentation chapter 8.4. Custom Mapping Types中查看如何执行此操作。

在实体列定义中,您指向类别类型:

/**
 * @var string
 * @ORM\Column(type="categories")
 */
protected $categories;

你在学说中注册的魔法是这样的:

'doctrine' => array(
    'configuration' => array(
        'orm_default' => array(
            'types' => array(
                'categories' => 'My\DBAL\Types\CategoriesCollection '
            )
        )
    )
)

然后是班级本身:

<?php

namespace My\DBAL\Types;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Common\Collections\Collection;

class CategoriesCollection extends \Doctrine\DBAL\Types\Type
{
    const NAME = 'categories';

    /**
     * @return string
     */
    public function getName()
    {
        return self::NAME;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getDoctrineTypeMapping('simple_array');
    }

    /**
     * @param Collection $collection
     * @param AbstractPlatform $platform
     * @return array
     */
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        $array = [];
        foreach($value as $category)
        {
            $category_id = $category->getId();
            array_push($array, $category_id);
        }
        return $array;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        $collection = new ArrayCollection();
        if ($value === null) {
            return $collection;
        }

        foreach($value as $category_id){
            $category = $this->em->getReference('Vendor\Bundle\Entity\Category', $category_id);
            $collection->add($category);
        }

        return $collection;
    }

    /**
     * @var EntityManager
     */
    protected $em;


    /**
     * @param EntityManager $entityManager
     */
    public function setEntityManager(EntityManager $entityManager)
    {
        $this->em = $entityManager;
    }
}

此解决方案几乎与其他解决方案相同,只有您使用Doctrine2内部实现。您仍然需要在DBAL类型中注册EntityManager并且不确定最简单的方法,以便我留给您。

在Symfony中,您可以在app / config / config.yml文件中注册自定义映射类型

doctrine:
  dbal:
    types:
      category_ids: Vendor\Bundle\Type\CategoriesCollection

您可以在捆绑包的引导顺序中注入EntityManager依赖项:

<?php

namespace Vendor\Bundle\Bundle;

use Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Trilations\TApp\CoreBundle\Type\CategoryCollectionType;

class VendorBundleBundle extends Bundle
{
    public function boot()
    {
        $em = $this->container->get('doctrine.orm.default_entity_manager');
        $categoryCollectionType = Type::getType('category_ids');
        $categoryCollectionType->setEntityManager($em);
    }
}

并将字段映射到正确的自定义映射:

Vendor\Bundle\Enitity\Product
  table: product
  fields:
    categories: category_ids