Symfony 2.8+, Doctrine Inheritance and Forms - forms

Before i start, Note that I'm learning symfony so keep that in mind ! I just want to understand how it works.
Here's what i am trying to achieve :
I would like to make a working crud example of entities inheritance using doctrine. So this is how my example looks like :
Abstract Parent class : character
Child class 1 : Magician
Child class 2 : Warrior
Child class 3 : Archer
So after reading some documentation i decided to use the STI (Single Table Inheritance) of Doctrine.
Parent class :
/**
* Character
*
* #ORM\Table(name="character")
* #ORM\Entity(repositoryClass="AppBundle\Repository\CharacterRepository")
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="discr", type="string")
* #ORM\DiscriminatorMap({"magician_db" = "Magician", "warrior_db" = "Warrior", "archer_db" = "Archer"})
*/
abstract class Character{
protected id;
protected name;
public function getId();
public function getName();
public function setName();
}
Child Class 1 :
class Warrior extends Character{
protected armor;
public function battleShout();
}
Child Class 2:
class Magician extends Character{
protected silk;
public function spellAnnounce();
}
Child Class 3:
class Archer extends Character{
protected leather;
public function arrows();
}
I managed to create the table in my db, and i successfully loaded my fixtures for tests purposes. I also made my main view work (listing all characters).
My Problem :
Now i want to be able to create, edit & delete a specific character in the list with a single form. So for example i would have a 'type' select field where i can select 'warrior' , 'magician' or 'archer' and then i would be able to fill in the specific fields of the chosen entity. So let's say i choose 'warrior' in the form, then i would like to be able to set the armor property (along with the parents one of course) and persist it in the database.
I don't know how to do it since my parent class is abstract so i can't create a form based on that object.
Thx in advance for your help, i really need it !
PS: If there is a better solution / implementation don't hesitate !

The easiest way is to provide all fields and to remove them according to the 'type' value.
To do that you have to implement the logic on the client side (for displaying purpose) and server side (so that the removed fields cannot be changed in your entity).
On the client side :
Use javascript to hide the types which can't be set for each 'type' change (you can use JQuery and the .hide() function).
On the server side:
Add a PRE_BIND event to your form type, to remove the fields from the form :
http://symfony.com/doc/current/components/form/form_events.html#a-the-formevents-pre-submit-event
Your Form should look like :
// ...
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
$form = $formFactory->createBuilder()
->add('type', ChoiceType::class)
->add('armor')
->add('silk')
->add('leather')
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$submittedData = $event->getData();
$form = $event->getForm();
switch($submittedData['type'])
{
case 'warrior':
$form->remove('silk');
$form->remove('leather');
break;
case 'magician':
$form->remove('armor');
$form->remove('leather');
break;
case 'archer':
$form->remove('armor');
$form->remove('silk');
break;
default:
throw new ...;
}
})
->getForm();
// ...
EDIT
To deal with Single Table Inheritance, you can't use an abstract class, the base class must be a normal entity.
In your form, just set the class as AppBundle\Character.
In your controller action which creates the character, you must initiate your entity with something like this :
if($request->isMethod('POST')){
// form has been submitted
switch($request->get('type'))
{
case 'warrior':
$entity = new Warrior();
...
}
}
else{
// form has not been submitted, default : Warrior
$entity = new Warrior();
}
By editing and removing the character, you can directly deal with the Character Entity.
I recommand to not let the user change the type by edit, see Doctrine: Update discriminator for SINGLE_TABLE Inheritance

Related

TYPO3 : How to determine child-objecttypes of specific parent domain-model?

I have some different domain-models, each being parent of different sub-models.
All of those domain-models extend themselves out of a basic model class and I want to write a general function in the base-model, that deals with subclasses of the current model. Therefore, I need to find a way, to dynamically get all child-model-classes of a given domain-model.
Can this be done somehow ? Perhaps via Object-Storage-Definitions or similar ?!
Update : as mentioned in the comment section, mny question had nothing to do with TYPO3, it was a general php-question .. solution for my question are reflection-classes.
I guess your question has nothing to do with TYPO3, so take a look at this general PHP question thread and possible solutions here.
You are talking about Database Relationships. Yes, this can be done in TYPO3.
Each model should be mapped to a table. So, let's take for example the Category domain model and parent property
class Category extends AbstractEntity
{
/**
* #var \TYPO3\CMS\Extbase\Domain\Model\Category
*/
protected $parent = null;
/**
* #return \TYPO3\CMS\Extbase\Domain\Model\Category
*/
public function getParent()
{
if ($this->parent instanceof \TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy) {
$this->parent->_loadRealInstance();
}
return $this->parent;
}
/**
* #param \TYPO3\CMS\Extbase\Domain\Model\Category $parent
*/
public function setParent(\TYPO3\CMS\Extbase\Domain\Model\Category $parent)
{
$this->parent = $parent;
}
The parent property will return the parent category. The same logic is when you want to get the childs.

Access other entities while creating a Doctrine entity

Say I have tables A, B in MySQL and Doctrine entity classes with the same names. Those entities are managed by Doctrine and are basically created according to Symfony/Doctrine docs.
Now I want to create entity C with columns: x, y. Whenever this entity is created or updated, I want to set the column values:
x: select count(*) from A where (some condition)
y: select sum(y) from B where (other condition)
pull some other data from A or B and store it as column value for C.
I want to do this in PHP and not use mysql triggers. I can't achieve from inside the Entity classes, because they don't have access to entity manager. I don't want to do this in the controller, as I want insert/update operations to be standardized, and I will need to do it from multiple controllers, and I generally don't think the controller is a good place for logic like this.
So I need some kind of class which manages entity C.
My question is: How do I call this manager class and where do I place it in Symfony? I am pretty sure this is a common need in Symfony (to access multiple entities while creating another entity), but I don't know how it is called and if there is a standard practice with them.
you can define service in app/config/services.yml and pass Entity manager as argument
services:
app.service.some_service:
class: AppBundle\Service\SomeService
arguments: ["#doctrine.orm.default_entity_manager"]
place your logic inside service
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Entity\SomeEntity;
class SomeService
{
/**
* #var EntityManagerInterface
*/
protected $entityManager;
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
public function getSomeEntity($id) {
$entity = $this->entityManager->getRepository(SomeEntity::class);
// do some work, return result..
}
}
call it from controller
$someService = $this->get('app.service.some_service');
$someService->getSomeEntity($id);
:)
I think you should create a Doctrine Event Subscriber as described in the documentation
I'll try to explain the basics.
1) Declare the service
services:
c_entity_counter_subscriber:
class: AppBundle\EventListener\CounterSubscriber
tags:
- { name: doctrine.event_subscriber, connection: default }
2) In the Subscriber count A and B properties
namespace AppBundle\EventListener;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use AppBundle\Entity\A;
use AppBundle\Entity\B;
use AppBundle\Entity\C;
class CounterSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->count($args);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->count($args);
}
public function count(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
if (!$entity instanceof C) {
return;
}
$entityManager = $args->getEntityManager();
// ... count/sum entities from A/B classes using $entityManager and update $entity
}
}

mapping the data of the child form on the parent form symfony2

I have created a Form Type that adds an autocomplete feature for Entities, however it requires some configuration for every Entity, i.e: I have to pass the configuration to the options array, so I decided to make a new FormType for each Entity using the AutoCompleteType I created and reuse them.However I want these Formtypes i.e: the ones for each particular Entity, to return the Entity when getData() is called on it, what happens now is that I have to first retrieve the field of ParentForm containing the AutoCompleteType then call getData() to retrieve my Entity.How can I map this information directly on the ParentForm?
//the FormType of Some Entity using the AutoComplete
...
class SomeEntityAutoCompleteType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array options){
$builder->add('some_entity', 'entity_autocomplete', array(...));
}
}
//the controller
public function someAction(){
$form = $this->get('form.factory')->create(new SomeEntityAutoCompleteType());
...
//I want the below line to return my entity
$form->getData();
//but I have to use this one right now
$form['some_entity']->getData()
}
note: I haven't actually tested the other approach but from what I understand of the Symfony Form Component it should be the way I described;
I solved it by setting the parent type of my SomeEntityAutoCompleteType to the main autocomplete type I had created and configuring the options using the setDefaultOptions() method.
//SomeEntityAutoCompleteType
public function setDefaultOption(OptionsResolverInterface $resolver){
$resolver->setDefaults(...);
}
public function getParent(){
return "autocomplete_type";//this is the main autocomplete type I mentioned
}

Add custom property to serialized object

I'm developing RESTful API for a web service. And I need to expose some properties that do not belong to an entity itself.
For example I have a Pizza entity object, it has it's own size and name properties. I'm outputting it in JSON format with FOSRestBundle and JMSSerializer. I've setup properties annotations for this entity to expose needed properties via serialization groups and it's working great.
But I need to add some properties that do not belong to the entity itself. For example I want my pizza to have property: isFresh that is determined by some PizzaService::isFresh(Pizza $pizza) service. How do I do this?
Should I inject some additional logic to serialization process (if so how)?
Should I create a wrapper entity with properties that I want to expose from original entity plus additional external properties?
Should I add property isFresh to the original Pizza entity and populate in in the controller before serialization?
Should I return additional data independent of entity data (in a sibling JSON properties for example)?
In other words: what are the best practices around this issue? Could you provide examples? Thank you.
I think you can do that with the VirtualProperty annotation :
/**
* #JMS\VirtualProperty
* #return boolean
*/
public function isFresh (){
...
}
Edit : another solution with the Accessor annotation
/** #Accessor(getter="getIsFresh",setter="setIsFresh") */
private $isFresh;
// ...
public function getIsFresh()
{
return $this->isFresh;
}
public function setIsFresh($isFresh)
{
$this->isFresh= $isFresh;
}
In your controller, you call the setIsFresh method
(See http://jmsyst.com/libs/serializer/master/reference/annotation)
I've decided to create my own class to serialize an entity.
Here's the example:
class PizzaSerializer implements ObjectSerializerInterface
{
/** #var PizzaService */
protected $pizzaService;
/**
* #param PizzaService $pizzaService
*/
public function __construct(PizzaService $pizzaService)
{
$this->pizzaService = $pizzaService;
}
/**
* #param Pizza $pizza
* #return array
*/
public function serialize(Pizza $pizza)
{
return [
'id' => $pizza->getId(),
'size' => $pizza->getSize(),
'name' => $pizza->getName(),
'isFresh' => $this->pizzaService->isFresh($pizza),
];
}
}
You just have to configure DC to inject PizzaService into the object serializer and then just call it like this from the controller:
$pizza = getPizzaFromSomewhere();
$pizzaSerializer = $this->get('serializer.pizza');
return $pizzaSerializer->serialize($pizza);
The object serializer will return an array that can be easily converted to JSON, XML, YAML or any other format by using real serializer like JMS Serializer. FOSRestBundle will do this automatically if you configured it so.

Conditional field validation that depends on another field

I need to change the validation of some field in a form. The validator is configured via a quite large yml file. I wonder if there is any way to do validation on two fields at once.
In my case I have two fields that cannot be both empty. At least one has to be filled.
Unfortunately till now I just could see that the validation are defined on a per-field basis, not on multiple fields together.
The question is: is it possible in the standard yml configurations to perform the aforementioned validation?
thanks!
I suggest you to look at Custom validator, especially Class Constraint Validator.
I won't copy paste the whole code, just the parts which you will have to change.
Extends the Constraint class.
src/Acme/DemoBundle/Validator/Constraints/CheckTwoFields.php
<?php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class CheckTwoFields extends Constraint
{
public $message = 'You must fill the foo or bar field.';
public function validatedBy()
{
return 'CheckTwoFieldsValidator';
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
Define the validator by extending the ConstraintValidator class, foo and bar are the 2 fields you want to check:
src/Acme/DemoBundle/Validator/Constraints/CheckTwoFieldsValidator.php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CheckTwoFieldsValidator extends ConstraintValidator
{
public function validate($protocol, Constraint $constraint)
{
if ((empty($protocol->getFoo())) && (empty($protocol->getBar()))) {
$this->context->addViolationAt('foo', $constraint->message, array(), null);
}
}
}
Use the validator:
src/Acme/DemoBundle/Resources/config/validation.yml
Acme\DemoBundle\Entity\AcmeEntity:
constraints:
- Acme\DemoBundle\Validator\Constraints\CheckTwoFields: ~