Synfony 2 File Uploader with Doctrine 2 - file-upload

I want to make a simple file uploader with Symfony 2 and Doctrine 2.
I've follow this tutorial :
http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html
and this one :
http://leny-bernard.com/fr/afficher/article/creer-un-site-facilement-en-symfony2-partie-4
Here is my Entity class :
namespace Luna\KBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\File;
/**
* Luna\KBundle\Entity\Media
*
* #ORM\Entity
*/
class Media
{
/**
* #var integer $id
*/
private $id;
/**
* #var string $title
*/
private $title;
/**
* #var text $description
*/
private $description;
/**
* #var string $author
*/
private $author;
/**
* #var string $source
*/
private $source;
/**
* #Assert\File(maxSize="6000000")
*/
private $paths;
/**
* #var string $type
*/
private $type;
/**
* #var Luna\KBundle\Entity\object
*/
private $idobject;
/***********************************METHODS***********************************/
/**
* Set idobject
*
* #param Luna\KBundle\Entity\Object $idobject
*/
public function setIdobject(\Luna\KBundle\Entity\object $idobject)
{
$this->idObject = $idObject;
}
/**
* Get idObject
*
* #return Luna\KBundle\Entity\Object
*/
public function getIdObject()
{
return $this->idObject;
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set description
*
* #param text $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* Get description
*
* #return text
*/
public function getDescription()
{
return $this->description;
}
/**
* Set author
*
* #param string $author
*/
public function setAuthor($author)
{
$this->author = $author;
}
/**
* Get author
*
* #return string
*/
public function getAuthor()
{
return $this->author;
}
/**
* Set source
*
* #param string $source
*/
public function setSource($source)
{
$this->source = $source;
}
/**
* Get source
*
* #return string
*/
public function getSource()
{
return $this->source;
}
/**
* Set paths
*
* #param string $paths
*/
public function setPaths($paths)
{
$this->paths = $paths;
}
/**
* Get paths
*
* #return string
*/
public function getPaths()
{
return $this->paths;
}
/**
* Set type
*
* #param string $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* Get type
*
* #return string
*/
public function getType()
{
return $this->type;
}
public function getAbsolutePath()
{
return null === $this->paths ? null : $this->getUploadRootDir().'/'.$this->paths;
}
public function getWebPath()
{
return null === $this->paths ? null : $this->getUploadDir().'/'.$this->paths;
}
protected function getUploadRootDir()
{
// the absolute directory path where uploaded documents should be saved
return __DIR__.'/../../../../web/'.$this->getUploadDir();
}
protected function getUploadDir()
{
// get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
return 'uploads/mediaobject';
}
}
The fact is that #Assert\File(maxSize="6000000") is not working : I don't have a file uploader but just a simple texte field ?!
How can I make this works correctly ?
Regards Guys :)
EDIT : Here my Form builder
namespace Luna\KBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class MediaInit extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title')
->add('description')
->add('author')
->add('source')
->add('paths')
->add('type')
->add('idObject')
;
}
}
And Here my twig template :
{% extends '::layout.html.twig' %}
{####################################### MEDIA INIT###########################}
{% block content %}
<h1>Creer un Media</h1>
Entrez les informations de votre media ici
<form action="{{ path('media_init') }}" method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<p>
<button type="submit">Creer</button>
</p>
</form>
{% endblock %}

When you want to upload a file you need to declare a path field & a virtual file field. So, your class needs to look like :
class Media
{
private $path;
/**
* #Assert\File(maxSize="6000000")
*/
private $file;
}
And your form:
class MediaInit extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('file');
}
}

It seems that $builder variable in MediaInit class is not initialized properly (with "Media" type).

Related

Undefined method 'encodePassword'. - error in Apiplateform security encoder undefined

I have a problem in my symfony security api in the service PasswordService. It can't find the encoder
I can't guess what should I do.
This is my security.yaaml file:
This is my code:
<?php
namespace App\Services;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class PasswordService
{
/**
* #var PasswordService
*/
private $userPasswordEncoder;
/**
* #param UserPasswordEncoderInterface $userPasswordEncoder
*/
public function __construct(UserPasswordEncoderInterface $userPasswordEncoder)
{
$this->userPasswordEncoder = $userPasswordEncoder;
}
/**
* #param object $entity
* #param string $password
* #return string
*/
public function encode(object $entity, string $password): string
{
return $this->userPasswordEncoder->encodePassword($entity, $password);
}
/**
* #param string $password
* #return int
*/
public function formatRequirement(string $password)
{
return preg_match('#^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W)#', $password);
}
/**
* #param object $entity
* #param string $password
* #return bool
*/
public function isValid(object $entity, string $password): bool
{
return $this->userPasswordEncoder->isPasswordValid($entity, $password);
}
}
There is no real error you just have a wrong annotation here:
/**
* #var PasswordService <========
*/
private $userPasswordEncoder;
If you remove it, it should be fine. (Or replace it with UserPasswordEncoderInterface)

Avoiding getters and setters in PHP 7.4

As PHP 7.4 supports typed class properties: https://www.php.net/manual/en/migration74.new-features.php#migration74.new-features.core.typed-properties. Looks like a lot of code could be eliminated, in particular getters and setters that in entities and DTOs that was responsible for controlling properties types. For example such snippet:
class Foo implements BarInterface
{
/**
* #var int
*/
protected $id;
/**
* #var int|null
*/
protected $type;
/**
* #return int
*/
public function getId(): int
{
return $this->id;
}
/**
* #param int $id
* #return $this
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* #return int|null
*/
public function getType(): ?int
{
return $this->type;
}
/**
* #param int|null $type
* #return $this
*/
public function setType(?int $type)
{
$this->type = $type;
return $this;
}
}
Can be refactored to:
class Foo implements BarInterface
{
public int $id;
public ?int $type;
}
Am I right that this is good idea? What should I take into consideration while making such refactoring?

Adding new entity Sylius

I created new entity in App\Entity\, and have this error:
Compile Error: Declaration of App\Entity\OrderItem::getVariant(): ?App\Entity\ProductVariantInterface must be compatible with Sylius\Component\Core\Model\OrderItem::getVariant(): ?Sylius\Component\Core\Model\ProductVariantInterface
This is my OrderItem.php:
<?php
namespace App\Entity;
use App\Entity\Constants\DeliveryLocationConstants;
use Sylius\Component\Core\Model\OrderItem as BaseOrderItem;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\OrderItemInterface as BaseOrderItemInterface;
/**
* #ORM\Entity()
* #ORM\Table(name="sylius_order_item")
*
* Class OrderItem
* #package App\Entity
*/
class OrderItem extends BaseOrderItem implements BaseOrderItemInterface
{
/** #var ProductVariantInterface */
protected $variant;
/** #var string */
protected $productName;
/** #var string */
protected $variantName;
/** #var string */
protected $optiune;
/**
* {#inheritdoc}
*/
public function getVariant(): ?ProductVariantInterface
{
return $this->variant;
}
/**
* {#inheritdoc}
*/
public function setVariant(?ProductVariantInterface $variant): void
{
$this->variant = $variant;
}
public function getProduct(): ?ProductInterface
{
return $this->variant->getProduct();
}
/**
* {#inheritdoc}
*/
public function getProductName(): ?string
{
return $this->productName ?: $this->variant->getProduct()->getName();
}
/**
* {#inheritdoc}
*/
public function getOptiune(): ?string
{
return $this->optiune;
}
/**
* {#inheritdoc}
*/
public function setOptiune(?string $optiune): void
{
$this->optiune = $optiune;
}
/**
* {#inheritdoc}
*/
public function setProductName(?string $productName): void
{
$this->productName = $productName;
}
/**
* {#inheritdoc}
*/
public function getVariantName(): ?string
{
return $this->variantName ?: $this->variant->getName();
}
/**
* {#inheritdoc}
*/
public function setVariantName(?string $variantName): void
{
$this->variantName = $variantName;
}
/**
* {#inheritdoc}
*/
public function equals(BaseOrderItemInterface $item): bool
{
return parent::equals($item) || ($item instanceof static && $item->getVariant() === $this->variant);
}
/**
* Returns sum of neutral and non neutral tax adjustments on order item and total tax of units.
*
* {#inheritdoc}
*/
public function getTaxTotal(): int
{
$taxTotal = 0;
foreach ($this->getAdjustments(AdjustmentInterface::TAX_ADJUSTMENT) as $taxAdjustment) {
$taxTotal += $taxAdjustment->getAmount();
}
foreach ($this->units as $unit) {
$taxTotal += $unit->getTaxTotal();
}
return $taxTotal;
}
/**
* Returns single unit price lowered by order unit promotions (each unit must have the same unit promotion discount)
*
* {#inheritdoc}
*/
public function getDiscountedUnitPrice(): int
{
if ($this->units->isEmpty()) {
return $this->unitPrice;
}
return
$this->unitPrice +
$this->units->first()->getAdjustmentsTotal(AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT)
;
}
/**
* {#inheritdoc}
*/
public function getSubtotal(): int
{
return $this->getDiscountedUnitPrice() * $this->quantity;
}
}
Error message is pretty clear. You must to redefine getVariant() method like this:
public function getVariant(): ?\Sylius\Component\Core\Model\ProductVariantInterface
{
return $this->variant;
}
because in PHP parent class BaseOrderItem persist you to return Core\Model\ProductVariantInterface in this method

Symfony API with FOSRestBundle : circular reference has been detected

I'm working on a symfony project to build a rest API, I have 4 entities related to each other like that :
I've installed FOSRestBundle to build just a GET web service, when i want to get a resource for example :
http://evaluation.dev/app_dev.php/api/families
I got this error :
message: "A circular reference has been detected (configured
limit:1).",
this is my controller :
<?php
namespace API\APIBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
class CartographyController extends Controller
{
/**
* #Rest\View()
* #Rest\Get("/posts")
* #param Request $request
* #return Response
*/
public function getPostsAction(Request $request)
{
$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
$posts = $this->get('doctrine.orm.entity_manager')
->getRepository('EvalBundle:Post')
->findAll();
return new Response($serializer->serialize($posts, 'json'));
}
/**
* #Rest\View()
* #Rest\Get("/employments")
* #param Request $request
* #return Response
*/
public function geEmploymentAction(Request $request)
{
$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
$employments = $this->get('doctrine.orm.entity_manager')
->getRepository('EvalBundle:Employment')
->findAll();
return new Response($serializer->serialize($employments, 'json'));
}
/**
* #Rest\View()
* #Rest\Get("/professions")
* #param Request $request
* #return Response
*/
public function geProfessionsAction(Request $request)
{
$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
$professions = $this->get('doctrine.orm.entity_manager')
->getRepository('EvalBundle:Profession')
->findAll();
return new Response($serializer->serialize($professions, 'json')); }
/**
* #Rest\View()
* #Rest\Get("/families")
* #param Request $request
* #return Response
*/
public function geFamiliesAction(Request $request)
{
$encoder = new JsonEncoder();
$normalizer = new GetSetMethodNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
$families = $this->get('doctrine.orm.entity_manager')
->getRepository('EvalBundle:Family')
->findAll();
return new Response($serializer->serialize($families, 'json'));
}
}
Family entity :
<?php
namespace EvalBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* Family
*
* #ORM\Table(name="family")
* #ORM\Entity(repositoryClass="EvalBundle\Repository\FamilyRepository")
*/
class Family
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* One Family has Many Professions.
* #ORM\OneToMany(targetEntity="Profession", mappedBy="family",orphanRemoval=true,cascade={"persist", "remove"},fetch="EAGER")
*/
protected $professions;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Family
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return mixed
*/
public function getProfessions()
{
return $this->professions;
}
/**
* #param mixed $professions
*/
public function setProfessions($professions)
{
$this->professions = $professions;
}
public function __toString() {
return $this->name;
}
}
Profession entity :
<?php
namespace EvalBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Profession
*
* #ORM\Table(name="profession")
* #ORM\Entity(repositoryClass="EvalBundle\Repository\ProfessionRepository")
*/
class Profession
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* Many professions have One Family.
* #ORM\ManyToOne(targetEntity="Family", inversedBy="professions",cascade={"persist", "remove"})
*/
public $family;
/**
* One Profession has Many Employment.
* #ORM\OneToMany(targetEntity="Employment", mappedBy="profession",cascade={"persist", "remove"}, orphanRemoval=true,fetch="EAGER")
*/
private $employments;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Profession
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return mixed
*/
public function getEmployments()
{
return $this->employments;
}
/**
* #param mixed $employments
*/
public function setEmployments($employments)
{
$this->employments = $employments;
}
/**
* #return mixed
*/
public function getFamily()
{
return $this->family;
}
/**
* #param mixed $family
*/
public function setFamily($family)
{
$this->family = $family;
}
public function __toString() {
return $this->name;
}
}
Post entity
<?php
namespace EvalBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Post
*
* #ORM\Table(name="post")
* #ORM\Entity(repositoryClass="EvalBundle\Repository\PostRepository")
*/
class Post
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* Many Posts have One Employment.
* #ORM\ManyToOne(targetEntity="Employment", inversedBy="posts", fetch="EAGER")
* #ORM\JoinColumn(name="employment_id", referencedColumnName="id",nullable=false)
*/
public $employment;
/**
* One Post has Many LevelRequired.
* #ORM\OneToMany(targetEntity="RequiredLevel", mappedBy="post")
*/
private $requiredLevels;
/**
* One Post has Many PostEvaluation.
* #ORM\OneToMany(targetEntity="PostEvaluation", mappedBy="post")
*/
private $postEvaluations;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Post
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return mixed
*/
public function getEmployment()
{
return $this->employment;
}
public function getProfession(){
return $this->employment->profession;
}
public function getFamily(){
return $this->employment->profession->family;
}
/**
* #param mixed $employment
*/
public function setEmployment($employment)
{
$this->employment = $employment;
}
public function __toString() {
return $this->name;
}
}
employment entity
<?php
namespace EvalBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Employment
*
* #ORM\Table(name="employment")
* #ORM\Entity(repositoryClass="EvalBundle\Repository\EmploymentRepository")
*/
class Employment
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* Many Employments have One Profession.
* #ORM\ManyToOne(targetEntity="Profession", inversedBy="employments",fetch="EAGER")
*/
public $profession;
/**
* One Employment has Many post.
* #ORM\OneToMany(targetEntity="Post", mappedBy="employment",cascade={"persist", "remove"}, orphanRemoval=true,cascade={"persist", "remove"})
*/
private $posts;
/**
* One Employment has One Grid.
* #ORM\OneToOne(targetEntity="Grid", mappedBy="employment")
*/
private $grid;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Employment
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* #return mixed
*/
public function getProfession()
{
return $this->profession;
}
/**
* #param mixed $profession
*/
public function setProfession($profession)
{
$this->profession = $profession;
}
/**
* #return mixed
*/
public function getPosts()
{
return $this->posts;
}
/**
* #param mixed $posts
*/
public function setPosts($posts)
{
$this->posts = $posts;
}
/**
* #return mixed
*/
public function getGrid()
{
return $this->grid;
}
/**
* #param mixed $grid
*/
public function setGrid($grid)
{
$this->grid = $grid;
}
public function __toString() {
return $this->name;
}
}
any solution please ?
For you example, you can avoid the CircularReference like this
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
$serializer = new Serializer(array($normalizer), array(new JsonEncoder()));
var_dump($serializer->serialize($org, 'json'));
But in your example, you don't use the FOSRestBundle for the view in your controller. The FOSRestController give you a handleView() and a view() method. Like this
class CartographyController extends FOSRestController
{
public function getPostsAction(Request $request)
{
$posts = $this->get('doctrine.orm.entity_manager')
->getRepository('EvalBundle:Post')
->findAll();
$view = $this->view($posts, 200);
return $this->handleView($view);
}
In this case, the serialiser is a service, this service is activated in config.yml :
In your app/config/config.yml
framework:
serializer: { enabled: true }
In order to avoid circularReference, you can do this.
In your app/config/services.yml
circular_reference_handler:
public: false
class: callback
factory: [AppBundle\Serializer\CircularHandlerFactory, getId]
serializer.normalizer.object:
class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
arguments: ["#serializer.mapping.class_metadata_factory", null, "#serializer.property_accessor"]
public: false
tags: [serializer.normalizer]
calls:
- method: setCircularReferenceLimit
arguments: [1]
- method: setCircularReferenceHandler
arguments: ["#circular_reference_handler"]
The factory can be like this:
namespace AppBundle\Serializer;
class CircularHandlerFactory
{
/**
* #return \Closure
*/
public static function getId()
{
return function ($object) {
return $object->getId();
};
}
}
Another idea is to use the GROUPS for the serializer. There is an annotation given by FosRestBundle :
#Rest\View(serializerGroups={"user"})
More information here:
https://symfony.com/doc/current/serializer.html#using-serialization-groups-annotations
Symfony2, FOSRestBundle. How to use group with JMSSerializerBundle?

How to avoid duplicates on ManyToMany relation with Doctrine2 and Zend Framework2?

The objective is to have 2 entity Article and Tag, with a many to many relation where the tags in Tag Table remain unique even if declare the same tag for other article.
I try to explain better with code:
Article Entity:
/**
* #ORM\Entity
* #ORM\Table(name="articles")
*/
class Article {
/**
* #ORM\Id #ORM\GeneratedValue(strategy="AUTO") #ORM\Column(type="integer")
* #var int
*/
protected $id;
/**
* #ORM\Column(type="string")
* #var string
*/
protected $name;
/**
* #ORM\ManyToMany(targetEntity="Tag\Entity\Tag",inversedBy="platforms",cascade={"persist","remove"})
*
* #return Tag[]
*/
protected $tags;
public function __construct() {
$this->tags = new ArrayCollection();
}
public function setId($id) {
$this->id = $id;
}
public function getId() {
return $this->id;
}
public function setName($name) {
$this->name = $name;
}
public function getName() {
return $this->name;
}
public function setTags($tags){
$this->tags = $tags;
}
public function setTagsFromArray(array $tags){
foreach($tags as $tag){
$this->tags->add(new Tag($tag));
}
}
/**
* Return the associated Tags
*
* #param boolean $getOnlyTagsName [optional] If set to true return a simple array of string (tags name).
* If set to false return array of Tag objects.
*
* #return Tag[]|string[]
*/
public function getTags($getOnlyTagsName=false) {
if ($getOnlyTagsName){
return array_map(function($i) { return $i->getName(); }, $this->tags->toArray());
}
return $this->tags;
}
public function addTags($tags) {
foreach($tags as $tag){
$this->tags->add($tag);
}
}
public function removeTags($tags) {
foreach ($tags as $tag){
$this->tags->removeElement($tag);
}
}
}
Tag Entity:
/**
* #ORM\Entity
* #ORM\Table(name="tags")
* ORM\HasLifecycleCallbacks // NOT USED
*/
class Tag {
/**
* #ORM\Id #ORM\Column(type="integer") #ORM\GeneratedValue
* #var int
*/
protected $id;
/**
* #ORM\Column(type="string",unique=true)
* #var string
*/
protected $name;
/**
* #ORM\Column(type="datetime",name="created_at")
* #var datetime
*/
protected $createdAt;
/**
* #ORM\Column(type="datetime",name="updated_at")
* #var datetime
*/
protected $updatedAt;
/**
* #ORM\ManyToMany(targetEntity="Article\Entity\Article", mappedBy="tags")
* var Tag[]
*/
protected $platforms;
/**
* Constructor
*
* #param string $name Tag's name
*/
public function __construct($name = null) {
$this->setName($name);
$this->setCreatedAt(new DateTime('now'));
$this->setUpdatedAt(new DateTime('now'));
}
/**
* Avoid duplicate entries.
*
* ORM\PrePersist // NOT USED
*/
public function onPrePersist(LifecycleEventArgs $args) {
}
/**
* Avoid duplicate entries.
*
* ORM\PreUpdate // NOT USED
*/
public function onPreUpdate(PreUpdateEventArgs $args) {
}
public function setId($id) {
$this->id = $id;
}
/**
* Returns tag's id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Sets the tag's name
*
* #param string $name Name to set
*/
public function setName($name) {
$this->name = $name;
}
/**
* Returns tag's name
*
* #return string
*/
public function getName() {
return $this->name;
}
public function setCreatedAt(DateTime $date) {
$this->createdAt = $date;
}
public function getCreatedAt() {
return $this->createdAt;
}
public function setUpdatedAt(DateTime $date) {
$this->updatedAt = $date;
}
public function getUpdatedAt() {
return $this->updatedAt;
}
}
Then I have the Article Form (with Article Fieldset) where there is a tagsinput (jquery plugin) element. So the form post it's like:
object(Zend\Stdlib\Parameters)[151]
public 'security' => string 'dc2a6ff900fbc87933e07bd35ef36709...' (length=65)
public 'article' =>
array (size=3)
'id' => string '' (length=0)
'name' => string 'Article 1' (length=13)
'tags' =>
array (size=3)
0 => string 'tag1' (length=3)
1 => string 'tag2' (length=4)
2 => string 'tag3' (length=7)
public 'submit' => string 'Add' (length=8)
A the first insert all goes well, but when I try to insert another article with one of the article1 tags I get the error:
An exception occurred while executing 'INSERT INTO tags (name, created_at, updated_at) VALUES (?, ?, ?)' with params ["tag2", "2014-04-26 22:05:37", "2014-04-26 22:05:37"]:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'tag2' for key 'UNIQ_6FBC94265E237E06'
I know that I can use prePersist and preUpdate event listner, but I made some test but I don't know how to use Unit Of Work to avoid duplicates.
You can't have the Tag's name property (and database field) defined as unique. You'll have the Integrity constraint violation every time you set a new tag with a previously picked tag name. You just need to control that a name is not picked for a tag related to the same article.
You could add a Zend\Validator\Db\NoRecordExists validator to the tagsinput element to achieve that. It would be something like that:
$tagsinput->getValidatorChain()->addValidator(
new NoRecordExists( array(
'adapter' => $this->getServiceManager()->get( 'Zend\Db\Adapter\Adapter' ),
'table' => 'tag',
'field' => 'name',
'message' => 'The tag name already exists',
'exclude' => 'article_id = ' . $article->getId(),
) );
--
Edit:
This solution won't work if there are more than one tag, when tagsinput's value will be an array. You could iterate through the array to validate each tag, but it's not an efficient solution, it would execute N queries. You could implement a similar validator which accepts a where clause like TAG_NAME IN ( 'tag1', 'tag2', 'tag3' )
--
Note that this validator doesn't work with Doctrine2; table, field and exclude parameters have database values.
This should work when adding tags to an existing article. To validate that a new article doesn't have duplicated tags, you could do it client side or you could write your own validator.