Este foro ya no está activo, así que no puedes publicar nuevas preguntas ni responder a las preguntas existentes.

Filtrar PersistentCollection de una relación ManyToMany

20 de abril de 2015

Buenas tardes,

Tengo que filtrar desde un array de objetos (Entidad) el contenido de una propiedad asociada con otra Entidad con una relación ManyToMany.

Mi problema es que lo que yo obtengo con get() de esa propiedad es una instancia de Doctrine\ORM\PersistentCollection, y esta clase:

  1. No me permite hacer una consulta con la función marching(Criteria $criteria), al menos no cuando se trata de una relación ManyToMany.
  2. No tiene una función con la que obtener el ArrayCollection con las entidades relacionadas al que poder hacerle esa consulta. Lo máximo que me devuelve es un array de objetos (Entidad).

Edito: Sí que tiene una función para obtener la propiedad coll que contiene el ArrayCollection con los objetos, es unwrap(). Pero en este caso contiene un array vacío

¿Cuál sería la forma más eficiente y "elegante" de hacerlo?

Pongo un resumen del código por si aclara más el caso:

class Anuncio
{
    /**
     * @var ArrayCollection
     *
     * @ORM\ManyToMany(targetEntity="Promocion", mappedBy="anuncios")
     */
    private $promociones;
}
 
class Promociones
{
    /**
     * @var ArrayCollection
     *
     * @ORM\ManyToMany(targetEntity="Anuncio", inversedBy="promociones")
     * @ORM\JoinTable(name="promociones_anuncios")
     */
    private $anuncios;
 
}
 
class AnuncioManager
{ 
    /**
     * @return array
     */
    public function obtenerAnuncios(){
 
        $anuncios = $this->repository->findAllOrderedByPosicion();
 
        foreach ($anuncios as $anuncio){
            $anuncio = $this->descartarPromocionesCaducadas($anuncio);
            $anuncios[$anuncio->getPosicion()] = $anuncio;
        }        
 
        return $anuncios;
    }
 
    /**
     * @param Anuncio $anuncio
     * @return Anuncio
     */
    public function descartarPromocionesCaducadas(Anuncio $anuncio){
 
        //Código que obtiene las promociones vigentes y las almacena en el objeto $anuncio
 
        return $anuncio;
    }
}

Como ya digo la cuestión no es resolverlo si no saber cuál es la mejor forma de resolver una situación así.

Muchas gracias de antemano :)


Respuestas

#1

He encontrado el método que me devuelve el atributo coll con un ArrayCollection, es la función unwrap() pero contiene un array vacío.

@hm_sergio

20 abril 2015, 17:53
#2

Buenas Sergio, desde mi punto de vista la solución puede estar enfocada en dos aspectos:

Facilidad de Desarrollo

Para lograr una implementación facil de realizar lo mejor es obtener los elementos de la colección directamente desde la entidad, haciendo algo como:

Class Anuncio
{
   public function getPromocionesActivas()
   {
       // filtramos la colección, y devolvemos los elementos que cumplen el filtro.
       return $this->getPromociones()->filter(function(Promociones $promocion){
          return $promocion->isActiva();
       });
   }
}

Asi, donde sea necesario obtener las promocionas activas de un anuncio, será muy facil de hacer, solo llamando al método getPromocionesActivas de la entidad Anuncio.

La desventaja de esta implementación es el rendimiento, ya que al ejecutar $this->getPromociones() estamos obteniendo todas las promociones del anuncio (tanto activas como caducadas), y si por ejemplo un anunció tiene muchas promociones (pongamos unas 50 o puede ser mucho más) y solo 4 están activas, estamos siempre obteniendo un monton de registros y gastando memoria sin ningún sentido.

Rendimiento

Si lo que se quiere es mantener el rendimiento de la aplicación, se me ocurre que lo mejor es crear un método en un Repositorio para las promociones:

Class PromocionesRepository
{
   public function findPromocionesActivasByAnuncio($anuncio)
   {
      return $this->createQueryBuilder('p')
                  ->where('p.anuncios = :anuncio')
                  ->andWhere('p.activa = true')
                  ->setParameter('anuncio', $anuncio)
                  ->getQuery()
                  ->getResult();
   }
}

Así, cuando necesitemos obtener las promociones activas de un anuncio, solo debemos llamar al método findPromocionesActivasByAnuncio del repositorio de la entidad Promociones pasandole el anuncio, y obtendremos solo las promociones que necesitamos sin desperdicio de memoria.

La desventaja que veo desde mi punto de vista al implementar esta solución, es que es un poco más complejo su uso, teniendo que obtener y usar la clase PromocionesRepository, donde tengamos que consultar las promociones activas de un anuncio.


Bueno espero sirva de algo esta explicación, Saludos!

@manuel_j555

22 abril 2015, 15:55
#3

@manuel_j555: Gracias por tu respuesta.

De las dos posibilidades que planteas me gusta más la segunda, por el tema de no meter más lógica en la clase de la Entidad.

En cuanto al acceso a la función del repositorio, no lo veo un inconveniente si usas servicios con inyección de dependencias, y los repositorios personalizados sobre Entidades que te permite usar Doctrine.

De esta forma podría añadir la función al repositorio personalizado de Anuncio y acceder directamente desde el mismo servicio AnuncioManager, o añadirlo al repositorio personalizado de Promocion y meter como dependencia el servicio PromocionManager que que me daría acceso al mismo.

El problema ahí es la consulta a la base de datos (en el repositorio), que sería algo más complicada de la que has puesto (estoy viendo la forma de hacerlo, porque a mí el dql se me atraganta), y habría que repetirla por cada anuncio, y lo que yo pretendía era que me la hiciera Doctrine a través de las funciones de sus colecciones.

La Entidad

/**
 * Anuncio
 *
 * @ORM\Table(name="anuncios")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\AnuncioRepository")
 */
class Anuncio{
 
}

El Repositorio Personalizado

class AnuncioRepository{
 
    /**
     * @param Anuncio $anuncio
     * @return array
     */
    public function findPromocionesActivasByAnuncio(Anuncio $anuncio){
 
       //la consulta de marras
    }
}

El Servicio

class AnuncioManager {
 
    /**
     * @var EntityManager
     */
    private $entityManager;
 
    /**
     * @var ProductoRepository
     */
    private $repository;
 
    /**
     *
     */
    function __construct(EntityManager $em ,$class)
    {
        $this->entityManager = $em;
        $this->repository = $em->getRepository($class);
    }
 
    /**
     * @return array
     */
     public function obtenerAnuncios(){
 
        return $this->repository->findAllOrderedByPosicion();
     }
 
    /**
     * @param $anuncios
     * @return array
     */
    public function obtenerPromocionesActivas($anuncios){
 
        $promocionesActivas = array();
 
        foreach ($anuncios as $anuncio){
            $promocionesActivas[$anuncio->getId()] = $this->repository->findPromocionesActivasByAnuncio($anuncio);
        }
 
        return $promocionesActivas;
    }
}

@hm_sergio

23 abril 2015, 17:12
#4

Al final lo resolví como me proponía @manuel_j555.

La consulta ha quedado así:

Repositorio:

/**
     * @param Anuncio $anuncio
     * @return array
     */
    public function findPromocionesVigentesByAnuncio(Anuncio $anuncio)
    {
        $now = new \DateTime('now');
        $qb =  $this->createQueryBuilder('p')
            ->join('p.anuncios', 'a')
            ->where('p.caducidad > :now')
            ->andWhere('p.estado = 0')
            ->andWhere('a.id = :anuncio')
            ->orderBy('p.orden', 'ASC')
            ->setParameter('anuncio', $anuncio->getId())
            ->setParameter('now', $now, Type::DATE);
 
        return $qb->getQuery()->getResult();
 
    }

El Servicio:

/**
     * @param array $anuncios
     * @return array
     */
    public function obtenerPromocionesVigentes($anuncios){
 
        $promocionesVigentes = array();
        foreach ($anuncios as $anuncio){
            $promocionesVigentes[$anuncio->getId()] = $this->promocionManager->getRepository()->findPromocionesVigentesByAnuncio($anuncio);
        }
 
        return $promocionesVigentes;
    }

@hm_sergio

29 abril 2015, 19:19