Optional embed form in Symfony 2 - symfony

I have two entities in my system: Person and Phone as the following code.
class Person
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=100)
*/
private $name;
/**
* #ORM\Column(type="string", length=100)
*/
private $lastName;
/**
* #ORM\OneToOne(targetEntity="Phone", cascade={"persist"})
*/
private $phone;
};
class Phone
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="PhoneType")
*/
private $type;
/**
* #ORM\Column(type="integer")
*/
private $countryCode;
/**
* #ORM\Column(type="integer")
*/
private $regionCode;
/**
* #ORM\Column(type="integer")
*/
private $number;
};
I also have a form to create and update a person (with the phone), so I have a PersonType that have a embed form representing a phone (PhoneType).
My problem is that a person can has optionally a phone, but if the person has a phone, all the phone fields are required. So, if user write nothing on all phone fields, this represent a person without phone and this case is valid. But if the user fills at least one form field, all other fields are required.
I try to take an approach by setting the phone on null if all phone fields are not filled, this was implemented on setPhone in Person entity. But having a null phone, Symfony tell me that all phone fields are required but are not filled.
I believe that Symfony will not validate the phone because I suppose that Symfony will apply the validation directly on person entity. Having a null phone, why tell me that all phone fields are not filled?
Is there a way to do what I want (preferably without modify all my controllers and form types, that is, at entity or validation component level)?
EDIT: Sorry, there is something not mentioned, if user fill a phone field, all phone fields need to be validated separately with different validators (to check if a field is a well formed number, to check correct length, etc). But if user leaves empty all phone fields, the per-field validation should be ignored.
Thanks!

I would try this method:
create additional method in your Phone entity which validates if all fields are null or all fields are not null (these two cases are correct) - and then return true. If some fields are null and some aren't return false. Add an assert annotation to your new method - this will be your new constraint.
/**
* #Assert\True(message = "Fill all fields or leave all them blank")
*/
And this should work.
For more information look here:
http://symfony.com/doc/master/book/validation.html#getters
edit:
Try this one:
define your custom validation method (this one which check if any of phone fields is filled) as Callback (at the top of the class):
* #Assert\Callback(methods={"checkPhoneFields"})
*/
class Phone {
Next you mark field wich have to be validated with validation group, eg.
/**
* #ORM\Column(type="string", length=16, groups={"phone_validation"})
*/
private $number;
And the last thing, in your custom constraint method you need to switch on "phone_validation" group if any of field isn't empty:
public function checkPhoneFields(ExecutionContext $context) {
if (/* fields are not empty */) {
$context->getGraphWalker()->walkReference($this, 'phone_validation', $context->getPropertyPath(), true);
}
And that should work.

For those passing by, with newer versions of Symfony you can do :
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$context->validate($this->getPhone(), 'phone', array("phone_validation"));
}
}

Do this:
First on your phone entity, declare the fields as "nullable" like this:
/**
* #ORM\Column(type="integer", nullable=true)
*/
private $countryCode;
This will allow these fields to have a null value.
Then you need to declare a validator that will check if either none of the fields or all of them are filled
class Phone {
...
/**
* #Assert\True(message = "Phone must be empty of fill all fields")
*/
public function isPhoneOK()
{
// check if no field is null or all of them are
// return false if these conditions are not met
}

It is also possible to use a very simple data Transformer to solve this problem.
Create your Phone data transformer :
class PhoneTransformer implements DataTransformerInterface
{
public function transform($phone)
{
if (is_null($phone))
return new Phone();
return $phone;
}
public function reverseTransform($phone)
{
if (is_null($phone))
return null;
if (!$phone->getType() && !$phone->getCountryCode() /* ... Test if phone is not valid here */)
return null;
return $phone;
}
Then simply prepend this transformer in your PhoneType form :
class PhoneType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
/* Add fields here :
->add( ... )
*/
->prependNormTransformer(new PhoneTransformer())
;
}
}
See http://symfony.com/doc/2.0/cookbook/form/data_transformers.html for more details on how implement data transformers.

frankie567's answer was useful to me. For symfony >= 3.0:
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$validator = $context->getValidator();
$validator->inContext($context)
->atPath('phone')
// you can pass custom validation instead of null, like new Assert\Valid()
->validate($this->getPhone(), null, array('phone_validation'));
}
}

Related

Symfony 3.4 entity extra columns in controller response

I have this entity:
<?php
//namespace
//use ...
class Guide
{
private $id;
//private ...
//getters
//setters
}
?>
In a controller I use the entity manager to retrieve the data of this entity.
$guides= $em->getRepository('AppBundle:Guide')
->findAll();
My entity has 4 parameters: id, name, pages, author.
Is there any way to add two extra parameters, that aren´t in the class declaration and I don´t want in the database, if the entity manager returns for example 3 rows, I want o add two extra values to each row and return the data, for example add two boolean values: ok => true, warning => false.
I have tried this:
foreach($guides as $guide){
$guide->ok=true;
$guide->warning=false;
}
If I dump $guides, I see the two parameters like this:
-id:1
-name:'Guide 1'
-pages:12
-author:'John'
+"ok":true
+"warning":false
But when I use this to send a response:
return new Response($serializer->serialize($guides, 'json'));
The two extra parameters aren´t in the response.
You could add a property to entity and do not tag it as a ORM\Column eg:
<?php
//...
/**
* #ORM\Entity
* #ORM\Table(name="guides")
*/
class Guide
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
* #ORM\Column(name="title", type="string")
*/
private $name;
public $myAdditionalProperty;
//...
And then set it in your controller:
foreach($guides as $guide){
$guide->myAdditionalProperty = "my amazing value";
}
Then you can serialize your data without having additional column in your table

Doctrine OneToMany relation does not fetch related entities

I am stuck at this case, I reproduced it in an example from symfony documentation, here it how it looks:
FIXTURES
/**
* #ORM\Entity
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", fetch="EAGER")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function products(): Collection
{
return $this->products;
}
public function id()
{
return $this->id;
}
}
and related Product class
/**
* #ORM\Entity
*/
class Product
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function __construct($category)
{
$this->category = $category;
}
public function id()
{
return $this->id;
}
public function category()
{
return $this->category;
}
}
TEST
Now I have this snippet of test code where I want to fetch Category and be able to get its Products:
$cat = new Category();
$prod = new Product($cat);
$this->entityManager->persist($prod);
$this->entityManager->persist($cat);
$this->entityManager->flush();
$crepo = $this->getEntityManager()->getRepository(Category::class);
$c = $crepo->findAll()[0];
var_dump(get_class($c->products()), $c->products()->count())
What I am getting is products of class PersistentCollection which is expected, but the count is 0 while there should be 1 product.
I can see that in the database I have proper category and product records with proper foreign key set.
WORKAROUND
I am debugging PersistentCollection for products and can see that its flag is set to initialized = true. With this I am able to force this to work by calling
$c->products()->setInitialized(false);
$c->products()->initialize();
But afaik this is not how it should work, should it ?
I managed to found an answer. It basically works but not when run in the same process. If I split the script in two - first one persists, second retrieves the data then the products collection will contain products related to category.
This is because when it is done in single process doctrine does not know that the category in question has products, and since it retrieves the same object it just saved and that was created few lines above, the entity manager won't populate the collection using database but will use the one from the category object. And the category object does not have products in products collection, since there is no call like $this->products()->add($category) neither in Product constructor or anywhere else. Only forcing to reinitialize the collection works since then it really retrieves products from database

Found the public method "add", but did not find a public "remove" in symfony2 entity

I get this exeption when I submit my form:
Found the public method "addRemote", but did not find a public "removeRemote" on class App\CoreBundle\Entity\Scene
The weired think is that the remove method exist ...
But i wrote it myself (When I did php app/console doctrine:generate:entities) doctrine didn't generated it. Did I make something wrong ?
/**
* #var array $remote
*
* #ORM\Column(name="remote", type="array", nullable=true)
*/
private $remote;
/**
* Set remote
*
* #param array $remote
* #return Scene
*/
public function addRemote($value, $key=null) {
if($key!=null){
$this->remote[$key] = $value;
}else{
$this->remote[] = $value;
}
return $this;
}
/**
* Remove remote
*/
public function removeRemote(){
unset($this->remote);
}
I allso tried:
/**
* Remove remote
*/
public function removeRemote($key=null){
if($key!=null && array_key_exists($key, $this->remote)){
unset($this->remote[$key]);
}
unset($this->remote);
return $this;
}
You have bigger problem than this; you are abusing your forms :)
Add.. and Remove... methods should be used for relations, not columns as per your code. Also, both add and remove methods must accept parameter that will be either added or removed.
If you still need an array, than getRemotes() method should return key=>value array. Adder and remover will later get that key, based on what user have picked in choice form type.

Symfony2, Doctrine, underscore in "findXxx" method name

Simple example, we've got
/**
* #ORM\Column(name="api_keyID", type="integer", nullable=false)
*/
private $api_keyID;
/**
* #return integer
*/
public function getApi_keyID()
{
return $this->api_keyID;
}
/**
* #param integer $api_keyID
* #return object
*/
public function setApi_keyID($data)
{
$this->api_keyID = $data;
return $this;
}
Look at method name and column name. When i try
//...
->findOneByApi_keyID($some);
I'm getting an error like
Entity 'entity\path' has no field 'apiKeyID'. You can therefore not call 'findOneByApi_keyID' on the entities' repository
So doctrine\symfony eats underscore? О.о And i cannot use it in column name?
is the way out
$repository->findBy(array('is_enabled' => true));
Founded here
Magic Doctrine2 finders when field has underscore?

Automatically updating created_by in the model

I wanted to have a created_by field for my model, say Product, that is automatically updated and I am using FOSUserBundle and Doctrine2. What is the recommended way of inputting the User id into Product?
Can I do it in the Product model? I am not sure how to do so and any help would be wonderful. Thanks!
I want to do something like this in the model, but I don't know how to get the user id.
/**
* Set updatedBy
*
* #ORM\PrePersist
* #ORM\PreUpdate
* #param integer $updatedBy
*/
public function setUpdatedBy($updatedBy=null)
{
if (is_null($updatedBy)) {
$updatedBy = $user->id;
}
$this->updatedBy = $updatedBy;
}
To relate the user to the product you want to associate the two entities:
http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="products")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* You may need to use the full namespace above instead of just User if the
* User entity is not in the same bundle e.g FOS\UserBundle\Entity\User
* the example is just a guess of the top of my head for the fos namespace though
*/
protected $user;
and for the automatic update field you may be after lifecyclecallbacks:
http://symfony.com/doc/current/book/doctrine.html#lifecycle-callbacks
/**
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Product
{
/**
* #ORM\PreUpdate
*/
public function setCreatedValue()
{
$this->created = new \DateTime();
}
}
EDIT
This discussion talks about getting the container in the entity in which case you could then get the security.context and find the user id from that if you mean to associate the current user to the product they edited:
https://groups.google.com/forum/?fromgroups#!topic/symfony2/6scSB0Kgds0
//once you have the container you can get the session
$user= $this->container->get('security.context')->getToken()->getUser();
$updated_at = $user->getId();
Maybe that is what you are after, not sure it is a good idea to have the container in the entity though, could you not just set the user on the product in the update action in your product controller:
public function updateAction(){
//....
$user= $this->get('security.context')->getToken()->getUser();
$product->setUser($user)
}

Resources