I'm having a hard time figuring out how to properly use Doctrine 2 with zend framework. I'm reading the docs and basing what I've done so far on that and the zendcasts. The problems actually start when I try to do relational stuff with my db, since I'm not so sure how to use doctrine collections. In my test case, I have an User entity:
class User
{
/**
* #var integer
* #Column (name="id", type="integer", nullable=false)
* #Id
* #GenerateValue(strategy="IDENTIY")
*
*/
private $id;
/**
* #Column(type="string",length=60,nullable=true)
* #var string
*/
private $email;
/**
*
* #param \Doctring\Common\Collections\Collection $property
* #OneToMany(targetEntity="Countries",mappedBy="user", cascade={"persist", "remove"})
*/
private $countries;
public function __get($property)
{
return $this->$property;
}
public function __set($property, $value)
{
$this->$property = $value;
}
}
Which is related to the countries entity:
class Countries {
/**
* #var integer
* #Column (name="id", type="integer", nullable=false)
* #Id
* #GenerateValue(strategy="IDENTIY")
*
*/
private $id;
/**
*
* #var string
* #Column(type="string")
*/
private $countryName;
/**
*
* #var User
* #ManyToOne(targetEntity="User")
* #JoinColumns({
* #JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
private $user;
public function __get($property)
{
return $this->$property;
}
public function __set($property, $value)
{
$this->$property = $value;
}
}
Now I can assign the countries from the controller with something like this:
$p1 = new \Federico\Entity\Countries();
$p1->countryName = 'Argentina';
$p2 = new \Federico\Entity\Countries();
$p2->countryName = 'España';
$u = new \Federico\Entity\User();
$u->firstname = 'John';
$u->lastname = 'Doe';
$u->id = 1;
which would show me this object:
object(Federico\Entity\User)[109]
private 'id' => int 1
private 'email' => null
private 'countries' =>
array
0 =>
object(Federico\Entity\Countries)[107]
private 'id' => null
private 'countryName' => string 'Argentina' (length=9)
private 'user' => null
1 =>
object(Federico\Entity\Countries)[108]
private 'id' => null
private 'countryName' => string 'España' (length=7)
private 'user' => null
public 'firstname' => string 'John' (length=4)
public 'lastname' => string 'Doe' (length=3)
If you pay attention to this, you'll see that the user property is set to null in the country objects. I don't understand if this is supposed to happen like this or not. Also, since users will be allowed to choose the countries from a checkbox list, and they'll be able to choose more than one country,...shouldn't the countries somehow be stored in the Db?
I don't see where you assign country to a user in your code. Anyway, you need to do two things:
Initialize $countries variable as a new Doctrine\Common\Collections\ArrayCollection in User constructor.
Manually connect country with user:
public function addCountry(Country $c)
{
if (!$this->countries->contains($c)) {
$this->countries->add($c);
$c->user = $user;
}
}
In doctrine 2 the use of the magic getters and setters is discouraged. As you can see they can cause problems for managing associations. Below is an example of how to manage the association that you have in your User entity.
namespace Whatever/Your/Namespace/Is;
use \Doctrine\Common\ArrayCollection;
class User
{
/**
* #Column (type="integer")
* #Id
* #var integer
*/
private $id;
/**
* #OneToMany(targetEntity="Country", mappedBy="user", cascade={"persist", "remove"})
* #var ArrayCollection
private $countries;
public function __construct()
{
$this->countries = new ArrayCollection();
}
public function getCountries()
{
return $this->countries;
}
public function setCountry(Country $country)
{
$this->country[] = $country;
$country->setUser($this);
return $this;
}
public function removeCountry(Country $country)
{
return $this->country->removeElement($country);
}
}
and for Country
class Country
{
/**
* #Id
* #Column(type="integer")
* #var integer
*/
private $id;
/**
* #Column(length=100)
* #var string
*/
private $country_name;
/**
* #ManyToOne(targetEntity="User", inversedBy="countries")
* #var User
*/
private $user;
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
public function getUser()
{
return $this->user;
}
// ...
}
Related
I need a relation uniderect one-to-one through composite key. I am using PostgreSQL.
#[Entity()]
#[Table(name: "passengers")]
class Passenger
{
#[Id]
#[Column(type: "uuid")]
private string $id;
#[Id]
#[Column(type: "uuid")]
private string $projectId;
#[OneToOne(targetEntity: "Ticket", cascade: ["persist"])]
#[JoinColumn(name: "ticket_id", referencedColumnName: "id")]
#[JoinColumn(name: "project_id", referencedColumnName: "project_id")]
private ?Ticket $ticket;
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #param string $id
*/
public function setId(string $id): void
{
$this->id = $id;
}
/**
* #return string
*/
public function getProjectId(): string
{
return $this->projectId;
}
/**
* #param string $projectId
*/
public function setProjectId(string $projectId): void
{
$this->projectId = $projectId;
}
/**
* #return Ticket|null
*/
public function getTicket(): ?Ticket
{
return $this->ticket;
}
/**
* #param Ticket|null $ticket
*/
public function setTicket(?Ticket $ticket): void
{
$this->ticket = $ticket;
}
}
#[Entity()]
#[Table(name: "tickets")]
class Ticket
{
#[Id]
#[Column(type: Types::STRING)]
private string $id;
#[Id]
#[Column(type: "uuid")]
private string $projectId;
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #param string $id
*/
public function setId(string $id): void
{
$this->id = $id;
}
/**
* #return string
*/
public function getProjectId(): string
{
return $this->projectId;
}
/**
* #param string $projectId
*/
public function setProjectId(string $projectId): void
{
$this->projectId = $projectId;
}
}
I'm trying to persist Passenger entity without Ticket entity:
$passenger = new Passenger();
$passenger->setId('07f5f7c8-def7-4fad-a146-b573421d8832');
$passenger->setProjectId('48ae51a1-4405-4f1d-8061-55a732d383a1');
$this->entityManager->persist($passenger);
$this->entityManager->flush();
I get an error:
Doctrine\DBAL\Exception\NotNullConstraintViolationException : An exception occurred while executing 'INSERT INTO passengers (id, project_id, ticket_id) VALUES (?, ?, ?)' with params ["07f5f7c8-def7-4fad-a146-b573421d8832", null, null]:
SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "project_id" of relation "passengers" violates not-null constraint
DETAIL: Failing row contains (07f5f7c8-def7-4fad-a146-b573421d8832, null, null).
I want to draw your attention to the fact that doctrine in sql overwrite the value of project_id for composite key to null.
If I persist Passenger by specifying the Ticket, everything works without errors.
Is this a doctrine bug or am I doing something wrong?
Error Message:
"A new entity was found through the relationship
'AppBundle\Entity\Category#products' that was not configured to
cascade persist operations for entity:
AppBundle\Entity\Product#00000000133d712400000000104ed306. To solve
this issue: Either explicitly call EntityManager#persist() on this
unknown entity or configure cascade persist this association in the
mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot
find out which entity causes the problem implement
'AppBundle\Entity\Product#__toString()' to get a clue."
/AppBundle/Entity/Product
class Product
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="decimal", scale=2)
*/
private $price;
/**
* #ORM\Column(type="text")
*/
private $description;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Product
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set price
*
* #param string $price
*
* #return Product
*/
public function setPrice($price)
{
$this->price = $price;
return $this;
}
/**
* Get price
*
* #return string
*/
public function getPrice()
{
return $this->price;
}
/**
* Set description
*
* #param string $description
*
* #return Product
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set category
*
* #param \AppBundle\Entity\Category $category
*
* #return Product
*/
public function setCategory(\AppBundle\Entity\Category $category = null)
{
$this->category = $category;
return $this;
}
/**
* Get category
*
* #return \AppBundle\Entity\Category
*/
public function getCategory()
{
return $this->category;
}
}
/AppBundle/Entity/Category
class Category
{
/**
* #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)
*/
protected $name;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
protected $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add product
*
* #param \AppBundle\Entity\Product $product
*
* #return Category
*/
public function addProduct(\AppBundle\Entity\Product $product)
{
$this->products[] = $product;
return $this;
}
/**
* Remove product
*
* #param \AppBundle\Entity\Product $product
*/
public function removeProduct(\AppBundle\Entity\Product $product)
{
$this->products->removeElement($product);
}
/**
* Get products
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getProducts()
{
return $this->products;
}
}
/AppBundle/Form/CategoryType
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
$builder->add('products', CollectionType::class, array(
'entry_type' => ProductType::class
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Category'
));
}
public function getName()
{
return 'category';
}
}
/AppBundle/Form/ProductType
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('price')
->add('description');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product',
));
}
public function getName()
{
return 'app_bundle_product_type';
}
}
/AppBundle/Controller/CategoryController
class CategoryController extends Controller
{
/**
* #Route("/category/new", name="category_new")
*
*/
public function newAction(Request $request)
{
$category = new Category();
$product = new Product();
$category->getProducts()->add($product);
$form = $this->createForm(CategoryType::class, $category);
$form->handleRequest($request);
if ($form->isValid() && $form->isSubmitted()) {
$em = $this->getDoctrine()->getManager();
$em->persist($category);
$em->flush();
}
return $this->render('category/new.html.twig', array(
'form' => $form->createView(),
));
}
}
/category/new.html.twig
{% extends 'base.html.twig' %}
{% block body %}
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Save</button>
{{ form_end(form) }}
{% endblock %}
You should change association in order to tell doctrine to persist all new category products
...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", cascade={"persist", "remove"})
*/
protected $products;
...
or persist it manually
...
$em->persist($category);
$em->persist($product);
$em->flush();
...
Also set category for product
...
$product = new Product();
$product->setCategory($category);
...
It is probably a very simple question but I've run out of juice here. Vat field is compulsory only when isVatable checkbox is check by user otherwise it can be ignored. How do I achieve this with group validation (annotations) in model class, not entity?
I checked Validation Groups and Group Sequence but to be honest didn't get my head around.
FormType
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('vat', 'text')
->add('isVatable', 'checkbox')
;
}
public function getName()
{
return 'user';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'My\FrontendBundle\Model\UserModel']
);
}
}
ModelClass
class UserModel
{
/**
* #Assert\NotBlank(message="Vat is required only when checkbox is checked.")
*/
protected $vat;
/**
* #var bool
*/
protected $isVatable = false;
}
I find that the #Assert\True() constraint on a method usually works well for me for these sorts of validation scenario. You can add some validation constraints to methods as well as properties, which is pretty powerful.
The basic idea is that you can create a method, give it this annotation - if the method returns true the validation passes; if it returns false it fails.
class UserModel
{
/**
* #var string
*/
protected $vat;
/**
* #var bool
*/
protected $isVatable = false;
/**
* #Assert\True(message="Please enter a VAT number")
*/
public function isVatSetWhenIsVatableChecked()
{
// if this property is unchecked we don't
// want to do any validation so return true
if (!$this->isVatable) {
return true;
}
// return true if $this->vat is not null
// you might want to add some additional
// validation here to make sure the
return !is_null($this->vat);
}
}
Additionally, you can map the error message to a specific form field with the error_mapping option in your FormType object, as documented here:
http://symfony.com/blog/form-goodness-in-symfony-2-1#error-mapping-fu
Hope this helps :)
Perhaps something like this in ...\Validator\Constraints:
VAT.php
use Symfony\Component\Validator\Constraint;
class VAT extends Constraint
{
public $message = 'VAT is compulsory for applicable items';
public $vat;
public $isVatable;
}
VATConstraint.php
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\PropertyAccess\PropertyAccess;
class VATValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$accessor = PropertyAccess::createPropertyAccessor();
$data = $accessor->getValue($this->context->getRoot(), 'data');
$vat = $data['vat'];
$isVatable = $data['isVatable'];
if ($isVatable && empty($vat)) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
return false;
}
return true;
}
}
User model Entity:
use YourBundle\Validator\Constraints as MyAssert;
class UserModel
{
/**
* #MyAssert\NotBlank(message="Vat is required only when checkbox is checked.")
*/
protected $vat;
...
}
You should use a Class Constraint Validator
First make your Constraint class and your validatorClass:
<?php
// AppBundle/Validator/Constraints/NotBlankIfTaxEnabled.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class NotBlankIfTaxEnabled extends Constraint
{
public $message = 'If isVat is enabled you have to enter a value in the Vat field.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and
<?php
// AppBundle/Validator/Constraints/NotBlankIfTaxEnabledValidator.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class NotBlankIfTaxEnabledValidator extends ConstraintValidator
{
public function validate($customer, Constraint $constraint)
{
if($customer->getIsVatable() && strlen($customer->getVat()) == 0)
{
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Then add de classcontraint to your Entity/Model
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Validator\Constraints as AppAssert;
/**
* Customer
*
* #ORM\Table()
* #ORM\Entity
* #AppAssert\NotBlankIfTaxEnabled
*/
class Customer
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=64)
*
*/
private $vat;
/**
* #var boolean
*
* #ORM\Column(name="taxEnabled", type="boolean")
*/
private $isVatable;
and do not forget to switch off the required attribute for both fields in your formType:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CustomerType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vat', NULL, array('required' => FALSE))
->add('isVatable', NULL, array('required' => FALSE))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Customer'
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_customer';
}
}
I am using doctrine 2 with oracle, the tables in the database has some triggers that generate the IDs, and my ID mapping of my tables is like the following:
/**
* #orm\Id
* #orm\Column(type="integer");
* #orm\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
and I have a OneToMany relation, with cascade={"persist"} but it is not working, I tried the same code with MySQL and it is working fine, but in oracle the last insert Id seems to always return 0 instead of the real id of the inserted row... and so the cascade persist is not working... is this a bug in doctrine or am I doing something wrong? any help?
After following the code it seems that the method
Doctrine\ORM\Id\IdentityGenerator::generate
is returning 0, I don't know why it is being invoked since the sequenceName is null (there is no sequence in the deffinition!
EDIT: Here are the entities:
The Client Entity:
/**
* #ORM\Entity
* #ORM\Table(name="clients")
**/
class Client {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
protected $id;
/** #ORM\Column(name="name",type="string",length=255,unique=true) */
protected $name;
/**
* #ORM\OneToMany(targetEntity="ContactInformation", mappedBy="client", cascade={"persist"})
**/
protected $contactInformations;
public function __construct() {
$this->contactInformations = new ArrayCollection();
}
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getContactInformations() {
return $this->contactInformations;
}
public function addContactInformations(Collection $contactInformations)
{
foreach ($contactInformations as $contactInformation) {
$contactInformation->setClient($this);
$this->contactInformations->add($contactInformation);
}
}
/**
* #param Collection $tags
*/
public function removeContactInformations(Collection $contactInformations)
{
foreach ($contactInformations as $contactInformation) {
$contactInformation->setClient(null);
$this->contactInformations->removeElement($contactInformation);
}
}
public function setContactInformations($contactInformations) {
$this->contactInformations = $contactInformations;
return $this;
}
}
The Contact Information Entity:
/**
* #ORM\Entity
* #ORM\Table(name="contact_informations")
**/
class ContactInformation {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="ContactInformationType")
* #ORM\JoinColumn(name="type_id", referencedColumnName="id")
**/
protected $type;
/** #ORM\Column(type="text") */
protected $value;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="contact_informations")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
**/
private $client;
public function getId() {
return $this->id;
}
public function getType() {
return $this->type;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getValue() {
return $this->value;
}
public function setValue($value) {
$this->value = $value;
return $this;
}
public function getClient() {
return $this->client;
}
public function setClient($client = null) {
$this->client = $client;
return $this;
}
}
Oracle doesn't support auto incrementing, so you cannot use the "IDENTITY" strategy in Doctrine. You'll have to use the "SEQUENCE" (or "AUTO") strategy.
When specifying "AUTO", Doctrine will use "IDENTITY" for MySql and "SEQUENCE" for Oracle.
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#identifier-generation-strategies
I have 2 entities
User and Address
User is mapped One to One (Unidirectional) to Address,
both have id/user_id field (primary) which is used for join
with cascade=persist and orphanRemoval=true
when I do
$user = $em->find('\Classes\User',1);
$user->removeAddress();
$em->flush();
it deletes the address part from database which is expected,
but why does it update id value to null in User object ?
/**
* #Entity
*/
class User
{
/** #Id #Column(type="integer") #GeneratedValue */
private $id;
/** #Column(length=100) */
public $name;
/** #OneToOne(targetEntity="Address", orphanRemoval=true)
* #JoinColumn(name="id", referencedColumnName="user_id")
*/
public $address;
public function removeAddress()
{
unset($this->address);
}
}
/**
* #Entity
*/
class Address
{
/** #Id #Column(type="integer") */
public $user_id;
/** #Column(length=100) */
public $street_name;
public function __construct($id, $name) {
$this->id = $id;
$this->name = $name;
}
}