---
title: Doctrine ORM en modulo PS 1.7+ — entidad completa
section: examples
slug: doctrine-entity-module
description: "Usar Doctrine ORM en modulos PrestaShop 1.7+: entidades, repositorios, migraciones, DQL queries y patron de servicio completo con DI."
keywords: prestashop doctrine orm entidad entity repository modulo symfony servicio
last_updated: 2025-01-15
source_url: "https://ayudaprestashop.es/examples/doctrine-entity-module"
---

# Doctrine ORM en modulo PS 1.7+ — entidad completa

> Usar Doctrine ORM en modulos PrestaShop 1.7+: entidades, repositorios, migraciones, DQL queries y patron de servicio completo con DI.

## Cuando usar Doctrine vs ObjectModel

| Criterio | ObjectModel | Doctrine ORM |
| --- | --- | --- |
| Compatibilidad | PS 1.5+ (todas) | PS 1.7.6+ (Symfony) |
| Complejidad | Sencillo, array $definition | Mas complejo, annotations |
| Relaciones | Manual (JOINs) | Automaticas (@ManyToOne, etc.) |
| Queries | DbQuery builder | DQL, QueryBuilder, Criteria |
| Cache | Manual | Doctrine cache integrado |
| Recomendado para | Modulos simples, tablas pocas | Modulos complejos, muchas relaciones |

## Estructura de archivos

*Arbol de archivos Doctrine en modulo*

```text
modules/ecom_reviews/
├── ecom_reviews.php
├── config/
│   └── services.yml
├── src/
│   ├── Entity/
│   │   ├── Review.php               # Entidad Doctrine
│   │   └── ReviewVote.php            # Entidad relacionada
│   ├── Repository/
│   │   └── ReviewRepository.php      # Repositorio custom
│   └── Service/
│       └── ReviewService.php         # Logica de negocio
└── upgrade/
    └── upgrade-2.0.0.php
```

## Entity con anotaciones

*src/Entity/Review.php*

```php
<?php
namespace EcomReviews\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use DateTime;

/**
 * @ORM\Table(name="PREFIX_ecom_review")
 * @ORM\Entity(repositoryClass="EcomReviews\Repository\ReviewRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Review
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private int $id;

    /**
     * @ORM\Column(name="id_product", type="integer")
     */
    private int $productId;

    /**
     * @ORM\Column(name="id_customer", type="integer", nullable=true)
     */
    private ?int $customerId;

    /**
     * @ORM\Column(name="customer_name", type="string", length=128)
     */
    private string $customerName;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private string $title;

    /**
     * @ORM\Column(type="text")
     */
    private string $content;

    /**
     * @ORM\Column(type="smallint")
     */
    private int $rating;

    /**
     * @ORM\Column(type="boolean")
     */
    private bool $validated = false;

    /**
     * @ORM\Column(name="date_add", type="datetime")
     */
    private DateTime $dateAdd;

    /**
     * Relacion One-to-Many: una review tiene muchos votos
     * @ORM\OneToMany(targetEntity="ReviewVote", mappedBy="review", cascade={"persist","remove"})
     */
    private Collection $votes;

    public function __construct()
    {
        $this->votes = new ArrayCollection();
        $this->dateAdd = new DateTime();
    }

    /**
     * Lifecycle callback: auto-set date_add
     * @ORM\PrePersist()
     */
    public function prePersist(): void
    {
        if (!isset($this->dateAdd)) {
            $this->dateAdd = new DateTime();
        }
    }

    // --- Getters ---
    public function getId(): int { return $this->id; }
    public function getProductId(): int { return $this->productId; }
    public function getCustomerName(): string { return $this->customerName; }
    public function getTitle(): string { return $this->title; }
    public function getContent(): string { return $this->content; }
    public function getRating(): int { return $this->rating; }
    public function isValidated(): bool { return $this->validated; }
    public function getDateAdd(): DateTime { return $this->dateAdd; }
    public function getVotes(): Collection { return $this->votes; }

    public function getUpvotes(): int
    {
        return $this->votes->filter(fn(ReviewVote $v) => $v->isPositive())->count();
    }

    // --- Setters ---
    public function setProductId(int $id): self { $this->productId = $id; return $this; }
    public function setCustomerId(?int $id): self { $this->customerId = $id; return $this; }
    public function setCustomerName(string $name): self { $this->customerName = $name; return $this; }
    public function setTitle(string $title): self { $this->title = $title; return $this; }
    public function setContent(string $content): self { $this->content = $content; return $this; }
    public function setRating(int $rating): self { $this->rating = max(1, min(5, $rating)); return $this; }
    public function validate(): self { $this->validated = true; return $this; }
    public function reject(): self { $this->validated = false; return $this; }
}
```

## Repository personalizado

*src/Repository/ReviewRepository.php*

```php
<?php
namespace EcomReviews\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use EcomReviews\Entity\Review;

class ReviewRepository extends EntityRepository
{
    /**
     * Reviews validadas de un producto, ordenadas por fecha
     */
    public function findValidatedByProduct(int $productId, int $limit = 20): array
    {
        return $this->createQueryBuilder('r')
            ->where('r.productId = :productId')
            ->andWhere('r.validated = :validated')
            ->setParameter('productId', $productId)
            ->setParameter('validated', true)
            ->orderBy('r.dateAdd', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    /**
     * Media de rating de un producto
     */
    public function getAverageRating(int $productId): ?float
    {
        $result = $this->createQueryBuilder('r')
            ->select('AVG(r.rating) as avg_rating')
            ->where('r.productId = :productId')
            ->andWhere('r.validated = true')
            ->setParameter('productId', $productId)
            ->getQuery()
            ->getSingleScalarResult();

        return $result ? round((float) $result, 1) : null;
    }

    /**
     * Reviews pendientes de moderacion (para admin)
     */
    public function findPendingReviews(int $page = 1, int $perPage = 20): array
    {
        return $this->createQueryBuilder('r')
            ->where('r.validated = false')
            ->orderBy('r.dateAdd', 'ASC')  // Las mas antiguas primero
            ->setFirstResult(($page - 1) * $perPage)
            ->setMaxResults($perPage)
            ->getQuery()
            ->getResult();
    }

    /**
     * Estadisticas de reviews por producto
     */
    public function getProductStats(int $productId): array
    {
        return $this->createQueryBuilder('r')
            ->select(
                'COUNT(r.id) as total',
                'AVG(r.rating) as average',
                'SUM(CASE WHEN r.rating = 5 THEN 1 ELSE 0 END) as five_stars',
                'SUM(CASE WHEN r.rating = 4 THEN 1 ELSE 0 END) as four_stars',
                'SUM(CASE WHEN r.rating = 3 THEN 1 ELSE 0 END) as three_stars',
                'SUM(CASE WHEN r.rating = 2 THEN 1 ELSE 0 END) as two_stars',
                'SUM(CASE WHEN r.rating = 1 THEN 1 ELSE 0 END) as one_star'
            )
            ->where('r.productId = :productId')
            ->andWhere('r.validated = true')
            ->setParameter('productId', $productId)
            ->getQuery()
            ->getSingleResult();
    }
}
```

## Servicio con inyeccion de dependencias

*src/Service/ReviewService.php*

```php
<?php
namespace EcomReviews\Service;

use Doctrine\ORM\EntityManagerInterface;
use EcomReviews\Entity\Review;
use EcomReviews\Repository\ReviewRepository;

class ReviewService
{
    private EntityManagerInterface $em;
    private ReviewRepository $repository;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
        $this->repository = $em->getRepository(Review::class);
    }

    public function createReview(
        int $productId,
        ?int $customerId,
        string $name,
        string $title,
        string $content,
        int $rating
    ): Review {
        $review = new Review();
        $review->setProductId($productId)
               ->setCustomerId($customerId)
               ->setCustomerName($name)
               ->setTitle($title)
               ->setContent($content)
               ->setRating($rating);

        $this->em->persist($review);
        $this->em->flush();

        return $review;
    }

    public function validateReview(int $reviewId): bool
    {
        $review = $this->repository->find($reviewId);
        if (!$review) return false;

        $review->validate();
        $this->em->flush();
        return true;
    }

    public function deleteReview(int $reviewId): bool
    {
        $review = $this->repository->find($reviewId);
        if (!$review) return false;

        $this->em->remove($review);
        $this->em->flush();
        return true;
    }
}
```

*config/services.yml*

```yaml
services:
  _defaults:
    public: true
    autowire: true

  ecom_reviews.service.review:
    class: EcomReviews\Service\ReviewService
    arguments:
      - '@doctrine.orm.entity_manager'
```

## Usar en un controller

*Usar el servicio en un hook del modulo*

```php
<?php
// En el modulo principal
public function hookDisplayProductExtraContent($params)
{
    // Obtener el servicio via container de Symfony
    /** @var ReviewService $reviewService */
    $reviewService = $this->get('ecom_reviews.service.review');

    // O acceder directamente al EntityManager
    $em = $this->get('doctrine.orm.entity_manager');
    $repo = $em->getRepository(Review::class);

    $reviews = $repo->findValidatedByProduct((int) $params['product']->id);
    $avgRating = $repo->getAverageRating((int) $params['product']->id);

    // ...
}
```

## Migraciones de schema

Doctrine no ejecuta migraciones automaticamente en PS. Debes crear las tablas en `install()` y actualizarlas en scripts de `upgrade/`. Puedes usar el Schema Tool de Doctrine para generar el SQL desde las entidades.

*install() con Doctrine Schema Tool*

```php
<?php
public function install()
{
    if (!parent::install()) return false;

    // Generar SQL desde entidades Doctrine
    try {
        $em = $this->get('doctrine.orm.entity_manager');
        $schemaTool = new \Doctrine\ORM\Tools\SchemaTool($em);
        $classes = [
            $em->getClassMetadata(Review::class),
            $em->getClassMetadata(ReviewVote::class),
        ];
        $schemaTool->createSchema($classes);
    } catch (\Exception $e) {
        // Fallback: SQL manual
        return $this->installSqlFallback();
    }

    return $this->registerHook('displayProductExtraContent');
}
```


---

*Fuente: [https://ayudaprestashop.es/examples/doctrine-entity-module](https://ayudaprestashop.es/examples/doctrine-entity-module). Version Markdown generada automaticamente para consumo por LLMs.*
