<?php
namespace App\EventSubscriber\Import\EntityMapper\VOD\Title;
use App\Repository\SonataClassificationCategoryRepository;
use App\Application\EntityImportBundle\Event\EmbeddableMapperEvent;
use App\Application\EntityImportBundle\Event\EntityPreInitEvent;
use App\Application\EntityImportBundle\Exception\RequiredPropertyException;
use App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue;
use App\Entity\SonataClassificationCategory;
use App\Entity\ProductGenre;
use App\Entity\VOD\Title;
use App\Entity\VOD\TitleEstInitialPriceCategory;
use App\Entity\VOD\TitleMediaFormat;
use App\Entity\VOD\TitleTvodInitialPriceCategory;
use App\Enum\Genre;
use App\Enum\VOD\EstPriceCategory;
use App\Enum\VOD\MediaFormat;
use App\Enum\VOD\TitleCategoryEnum;
use App\Enum\VOD\TitleLanguageEnum;
use App\Enum\VOD\TvodPriceCategory;
use App\EventSubscriber\Import\EntityMapper\BasePreInitSubscriber;
use App\Repository\ProductRepository;
use App\Repository\VOD\TitleRepository;
use App\Util\Helper\StringUtils;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class EntityPreInitSubscriber.
*/
class EntityPreInitSubscriber extends BasePreInitSubscriber
{
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $eventDispatcher;
/**
* The log manager.
*
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* The instance of the title repository.
*
* @var \App\Repository\VOD\TitleRepository
*/
private $titleRepository;
/**
* The instance of the product repository.
*
* @var \App\Repository\ProductRepository
*/
private $productRepository;
/**
* The instance of the classification category repository.
*
* @var \App\Entity\SonataClassificationCategoryRepository
*/
private $categoryRepository;
/**
* EntityPreInitSubscriber constructor.
*
* @param \Doctrine\ORM\EntityManagerInterface $entityManager
* The entity manager
* @param \Psr\Log\LoggerInterface $logger
* The log manager
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher
* @param \App\Repository\VOD\TitleRepository $titleRepository
* The title repository
* @param \App\Repository\ProductRepository $productRepository
* The product repository
* @param \App\Entity\SonataClassificationCategoryRepository $categoryRepository
* The classification category repository
*/
public function __construct(
EntityManagerInterface $entityManager,
LoggerInterface $logger,
EventDispatcherInterface $eventDispatcher,
TitleRepository $titleRepository,
ProductRepository $productRepository,
SonataClassificationCategoryRepository $categoryRepository
) {
parent::__construct($entityManager);
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->titleRepository = $titleRepository;
$this->productRepository = $productRepository;
$this->categoryRepository = $categoryRepository;
}
public static function getSubscribedEvents()
{
return [
EntityPreInitEvent::class => 'onEntityPreInit',
];
}
/**
* Public callback for the pre init event.
*
* Responsible for providing the mapping between parsed csv entry row and Title entity.
* The reason why we need pre-init event, rather than the default one, is because in
* this case we need a little bit more advanced logic when deciding how we will handle
* the entity instance - whether to load an existing one or create one and this cannot
* be achieved by simply providing or omitting an entity id.
*
* @param \App\Application\EntityImportBundle\Event\EntityPreInitEvent $event
* The instance of the dispatched event
*/
public function onEntityPreInit(EntityPreInitEvent $event): void
{
if (Title::class !== $event->getTargetClassName()) {
return;
}
$configuredProperties = $event->getConfigurationProperties();
// Firstly, we need to find out whether or not there is an existing entity
// that we can update, otherwise we will just create a new instance of the
// Title entity. To do that, we will grab a reference to some of the
// mandatory properties and use their values to look for a record.
[$navCodeProperty, $extendedCodeProperty, $languageProperty] = [
$this->filterConfiguredProperty('navCode', $configuredProperties, true),
$this->filterConfiguredProperty('extendedNavCode', $configuredProperties, true),
$this->filterConfiguredProperty('language', $configuredProperties, true),
];
if (null === $navCodeProperty || null === $extendedCodeProperty || null === $languageProperty) {
$event->setSkipRowProcessing(true);
return;
}
$data = $event->getData();
$navCode = $this->getValue($navCodeProperty, $data);
$extendedNavCode = $this->getValue($extendedCodeProperty, $data);
$language = $this->getValue($languageProperty, $data);
// The properties are mandatory, but we will check anyway whether we can get the data
// because we need all of them in order to try and find an existing title entity.
if (!$navCode || !$extendedNavCode || !$language || !TitleLanguageEnum::accepts($language)) {
$event->setSkipRowProcessing(true);
return;
}
if (!$entity = $this->titleRepository->findForEntityImport($navCode, $extendedNavCode, $language)) {
$entity = new Title();
$entity->setExtendedNavCode($extendedNavCode);
$entity->setLanguage($language);
// We did not find an existing Title entity, therefore we created a new one instead.
// But in order to proceed with the import process, first we need to find a valid
// product based on the navision title code, in order to setup the product reference.
// A Title record is only valid when we have a product to attach it to.
if (!$product = $this->productRepository->findOneByNavCode($navCode)) {
$event->setSkipRowProcessing(true);
return;
}
$entity->setProduct($product);
}
// Go through all, but the embeddable properties.
foreach ($configuredProperties as $property) {
if ($property->isEmbeddable()) {
continue;
}
$callback = StringUtils::camelize('handle_' . $property->getName());
try {
if (method_exists($this, $callback)) {
$this->{$callback}($entity, $property, $data);
} else {
$this->defaultPropertyHandler($entity, $property, $data);
}
} catch (\LogicException | RequiredPropertyException | \UnexpectedValueException $ex) {
$this->logger->error($ex->getMessage());
$event->setSkipRowProcessing(true);
}
}
// Go through all embeddable properties.
$this->eventDispatcher->dispatch(new EmbeddableMapperEvent(
$entity,
$event->getEmbeddedProperties(),
$data
));
$event->setEntity($entity);
}
/**
* Responsible for applying default entity mapping for given property.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function defaultPropertyHandler(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
$this->setPropertyValue($entity, $property, $this->getValue($property, $data, $entity, true));
}
/**
* Responsible for applying mapping for enumerable category type.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleCategoryType(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
$value = $this->getValue($property, $data, $entity, true);
if (!TitleCategoryEnum::accepts($value)) {
throw new \UnexpectedValueException("Invalid value provided for property {$property->getName()}.");
}
$this->setPropertyValue($entity, $property, $value);
}
/**
* Responsible for providing mapping for product genres collection.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleGenres(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
if (!$value = $this->getPropertyValue($property, $data, $entity)) {
return;
}
if (!$product = $entity->getProduct()) {
return;
}
if (!$parts = array_map('trim', explode(',', $value))) {
return;
}
$genres = new ArrayCollection();
foreach ($parts as $item) {
if (Genre::accepts($item)) {
$genre = new ProductGenre();
$genre->setGenre($item);
$genre->setProduct($product);
$genres->add($genre);
}
}
if (!$genres->isEmpty()) {
$product->getGenres()->clear();
$product->setGenres($genres);
} elseif ($genres->isEmpty() && $this->getAllowedNullableCharacter() === $value) {
$product->getGenres()->clear();
}
}
/**
* Responsible for providing mapping for entity aspect ratio.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleAspectRatio(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
if (!$value = $this->getValue($property, $data, $entity)) {
return;
}
$aspectRatio = $this->categoryRepository->findByNameAndContext(trim($value));
if ($aspectRatio instanceof Category) {
$this->setPropertyValue($entity, $property, $aspectRatio);
} elseif ($this->getAllowedNullableCharacter() === $value) {
$this->setPropertyValue($entity, $property, $value);
}
}
/**
* Responsible for providing mapping for the media IDs collection.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleMediaIdFormat(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
if (!$value = $this->getValue($property, $data, $entity, true)) {
return;
}
if (!MediaFormat::accepts($value)) {
throw new \UnexpectedValueException("Invalid value provided for property {$property->getName()}.");
}
$currentMediaFormats = [];
foreach ($entity->getMediaFormats() as $mediaFormat) {
$currentMediaFormats[] = $mediaFormat->getFormat();
}
// We already have an existing media format, do not process further.
// Media formats have also auto generated IDs, so this process should
// be handle carefully.
if (in_array($value, $currentMediaFormats)) {
return;
}
// No media format found, create new entity and proceed.
$mediaFormat = new TitleMediaFormat();
$mediaFormat->setFormat($value);
$mediaFormat->setTitle($entity);
$entity->addMediaFormat($mediaFormat);
$this->getEntityManager()->persist($mediaFormat);
}
/**
* Responsible for providing mapping for the initial EST price category.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleEstPriceCategory(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
if (!$value = $this->getValue($property, $data, $entity, true)) {
return;
}
$collection = $entity->getEstInitialPriceCategories();
$collection = $collection->filter(fn (TitleEstInitialPriceCategory $category) => empty($category->getCountry()));
if ($this->getAllowedNullableCharacter() === $value || !EstPriceCategory::accepts($value)) {
throw new \UnexpectedValueException("Invalid value provided for property {$property->getName()}.");
}
// We do not have any categories added yet for this VET, so create the initial one.
if ($collection->isEmpty()) {
$priceCategory = new TitleEstInitialPriceCategory();
$priceCategory->setTitle($entity);
$priceCategory->setCategory($value);
$entity->addEstInitialPriceCategory($priceCategory);
$this->getEntityManager()->persist($priceCategory);
return;
}
// We have a valid price category entity /managed at some point/ so we either
// update it or remove it from the collection, depending on the value.
$initialCategory = $collection->first();
// If we are up to this point, that means that we already have managed price category instance
// and we simply want to update it's value.
$initialCategory->setCategory($value);
$this->getEntityManager()->persist($initialCategory);
}
/**
* Responsible for providing mapping for the initial TVOD price category.
*
* @param \App\Entity\VOD\Title $entity
* The managed entity
* @param \App\Application\EntityImportBundle\ValueObject\ConfigItemPropertiesValue $property
* The property to map
* @param array $data
* The parsed data of the currently managed row
*/
protected function handleTvodPriceCategory(Title $entity, ConfigItemPropertiesValue $property, array $data): void
{
if (!$value = $this->getValue($property, $data, $entity, true)) {
return;
}
$collection = $entity->getTvodInitialPriceCategories();
$collection = $collection->filter(fn (TitleTvodInitialPriceCategory $category) => empty($category->getCountry()));
if ($this->getAllowedNullableCharacter() === $value || !TvodPriceCategory::accepts($value)) {
throw new \UnexpectedValueException("Invalid value provided for property {$property->getName()}.");
}
// We do not have any categories added yet for this VET, so create the initial one.
if ($collection->isEmpty()) {
$priceCategory = new TitleTvodInitialPriceCategory();
$priceCategory->setTitle($entity);
$priceCategory->setCategory($value);
$entity->addTvodInitialPriceCategory($priceCategory);
$this->getEntityManager()->persist($priceCategory);
return;
}
// We have a valid price category entity /managed at some point/ so we either
// update it or remove it from the collection, depending on the value.
$initialCategory = $collection->first();
// If we are up to this point, that means that we already have managed price category instance
// and we simply want to update it's value.
$initialCategory->setCategory($value);
$this->getEntityManager()->persist($initialCategory);
}
}