I created my first extbase extension which works fine. Now i want to enter address informations into my extension, like city, zip code and street.
I preffer to create a relation to "tt_address". My aim is to create a 1:N relation from "tt_address" to my extension.
But the extension "tt_address" is based on "pi_base", how can i handle this issue? How must my "tca" look?
How can i create a record form my "createAction" into the "tt_address" table, should i need the object manager?
Is there a example on web?
Update, i extend my model with street, zip and city:
/**
* street
*
* #var string
*/
protected $street;
/**
* street
*
* #var string
*/
protected $zip;
/**
* street
*
* #var string
*/
protected $city;
public function setStreet($street)
{
$this->street = $street;
}
public function getStreet()
{
return $this->street;
}
public function setCity($city)
{
$this->city = $city;
}
public function getCity()
{
return $this->city;
}
public function setZip($zip)
{
$this->zip = $zip;
}
public function getZip()
{
return $this->zip;
}
After this i extend my typoscript setup.txt
plugin.tx_oaevents {
view {
templateRootPath = {$plugin.tx_oaevents.view.templateRootPath}
partialRootPath = {$plugin.tx_oaevents.view.partialRootPath}
layoutRootPath = {$plugin.tx_oaevents.view.layoutRootPath}
}
persistence {
storagePid = {$plugin.tx_oaevents.persistence.storagePid},148
classes {
Mab\Oavents\Domain\Model\Events {
mapping {
tablename = tx_nnaddress_domain_model_address
columns {
zip.mapOnProperty = zip
street.mapOnProperty = street
city.mapOnProperty = city
}
}
}
}
}
And in last step i extend my extension TCA.
address' => array(
'exclude' => 1,
'label' => 'Events',
'config' => array(
'type' => 'select',
'foreign_table' => 'tt_address',
'minitems' => 0,
'maxitems' => 1,
),
),
You need to create a model and map it in TS to table tt_address. In this mapping you need to set each field from your model to a column in table that you want to map. When you will have model and mapping everything else is the same. You create controller and repository for your model and you can create createAction that will insert something to table tt_address
Related
I'm having a REST-API built in Symfony3.
As an example here are the API-fields of Price in a form, made with the FormBuilderInterface. The code-example below is of ApiBundle/Form/PriceType.php
class PriceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, array(
'description' => 'Name',
))
->add('price_category', EntityPublicKeyTextType::class, array(
'class' => 'MyCustomBundle:PriceCategory',
'property_path' => 'priceCategory',
))
The issue is about good response messages of fields which have e.g. a validation error. For default symfony-types (e.g. IntegerType, TextType) it can find the property_path automatically and hands me out an useful error message. Here is the API-response with two errors:
name can be resolved in a good way (because I see what field it is about,
for price_category it can't resolve it (second message).
{
"name": [
"This value is too long. It should have 50 characters or less."
],
"0": "This value should not be null."
}
To resolve the issue. I add 'property_path' => 'priceCategory' for the field price_category. The value of property_path is matching with BaseBundle/Entity/Price.php where the var protected $priceCategory; is defined.
After adding property_path the error message looks fine.
{
"name": [
"This value is too long. It should have 50 characters or less."
],
"price_category": [
"This value should not be null."
]
}
The class of price_category is EntityPublicKeyTextType which is abstracted from TextType (which can do errors just fine).
Therefore I have the following question: What do i have to add to my inherited class EntityPublicKeyTextType to avoid adding the property_path for all fields by hand?
Any hint to fix this is highly welcome
Best endo
EDIT:
EntityPublicKeyTextType:
class EntityPublicKeyTextType extends AbstractType
{
/**
* #var ObjectManager
*/
private $om;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om)
{
$this->om = $om;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new ObjectToPublicKeyTransformer(
$this->om,
$options['class'],
$options['public_key'],
$options['remove_whitespaces'],
$options['multiple'],
$options['string_separator'],
$options['extra_find_by']
);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setRequired(array(
'class',
'public_key'
))
->setDefaults(array(
'multiple' => false,
'string_separator' => false,
'extra_find_by' => array(),
'remove_whitespaces' => true,
));
}
public function getParent()
{
return TextType::class;
}
public function getBlockPrefix()
{
return 'entity_public_key_text';
}
}
ObjectToPublicKeyTransformer:
class ObjectToPublicKeyTransformer implements DataTransformerInterface
{
/**
* #var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* #var ObjectManager
*/
private $om;
/**
* #var string
*/
private $class;
/**
* #var string|string[]
*/
private $publicKey;
/**
* #var bool
*/
private $removeWhitespaces;
/**
* #var boolean
*/
private $multiple;
/**
* #var boolean|string
*/
private $stringSeparator;
/**
* #var array
*/
private $extraFindBy;
public function __construct(
ObjectManager $om,
string $class,
$publicKey,
bool $removeWhitespaces,
bool $multiple = false,
$stringSeparator = false,
array $extraFindBy = array(),
PropertyAccessorInterface $propertyAccessor = null
) {
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->om = $om;
$classMetadata = $om->getClassMetadata($class);
$this->class = $classMetadata->getName();
$this->publicKey = $publicKey;
$this->stringSeparator = $stringSeparator;
$this->multiple = $multiple;
$this->extraFindBy = $extraFindBy;
$this->removeWhitespaces = $removeWhitespaces;
}
/**
* Transforms an object / Collection of objects to a publicKey string / array of publicKey strings.
*
* #param object|Collection $object
* #return string|array
*/
public function transform($object)
{
if (null == $object) {
return null;
}
if (is_array($this->publicKey)) {
$publicKey = $this->publicKey[0];
} else {
$publicKey = $this->publicKey;
}
if ($this->multiple) {
if ($object instanceof Collection) {
$values = array();
foreach ($object as $objectItem) {
$values[] = (string)$this->propertyAccessor->getValue($objectItem, $publicKey);
}
if ($this->stringSeparator) {
return implode($this->stringSeparator, $values);
}
return $values;
}
} else {
return (string)$this->propertyAccessor->getValue($object, $publicKey);
}
}
/**
* Transforms an publicKey string / array of public key strings to an object / Collection of objects.
*
* #param string|array $value
* #return object|Collection
*
* #throws TransformationFailedException if object is not found.
*/
public function reverseTransform($value)
{
if (null === $value) {
return $this->multiple ? new ArrayCollection() : null;
}
if (is_array($this->publicKey)) {
$publicKeys = $this->publicKey;
} else {
$publicKeys = array($this->publicKey);
}
if ($this->multiple) {
if ($this->stringSeparator) {
$value = explode($this->stringSeparator, $value);
}
if (is_array($value)) {
$objects = new ArrayCollection();
foreach ($value as $valueItem) {
foreach ($publicKeys as $publicKey) {
$object = $this->findObject($valueItem, $publicKey);
if ($object instanceof $this->class) {
$objects->add($object);
break;
}
}
}
return $objects;
}
}
foreach ($publicKeys as $publicKey) {
$object = $this->findObject($value, $publicKey);
if ($object instanceof $this->class) {
return $object;
}
}
return $this->multiple ? new ArrayCollection() : null;
}
private function findObject($value, $publicKey)
{
if ($this->removeWhitespaces) {
$value = str_replace(' ', '', $value);
}
$findBy = array_merge([$publicKey => $value], $this->extraFindBy);
$object = $this->om->getRepository($this->class)->findOneBy($findBy);
return $object;
}
}
It would be useful if you also provide your Price model/entity class. It seems that you are using camel case for the property name in your model (priceCategory) and then you use snake case in your form (price_category).
If you use the same convention for the model and the form, the validation errors will automatically map to the correct property.
The explanation is that Symfony's mappers can still map your fields by transforming snake to camel case and vice versa, that's why your form is still working and submitting values even without using the property_path option. But the problem is that the validator does not do this mapping and cannot match the correct property (price_category -> priceCategory).
How to unit test Symfony 4 Form with EntityType field
When I run my test:
$ ./vendor/bin/simple-phpunit tests/Unit/Form/ProductFormTest.php
This is the output in my terminal:
PHPUnit 6.5.8 by Sebastian Bergmann
and contributors.
Runtime: PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 with Xdebug
2.7.0alpha2-dev Configuration: /var/www/project/phpunit.xml.dist
Testing App\Tests\Unit\Form\ProductFormTest E
1 / 1 (100%)
Time: 551 ms, Memory: 6.00MB
There was 1 error:
1) App\Tests\Unit\Form\ProductFormTest::formSubmitsValidData
Symfony\Component\Form\Exception\RuntimeException: Class
"App\Entity\Supplier" seems not to be a managed Doctrine entity. Did
you forget to map it?
/var/www/project/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:205
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:858
/var/www/project/vendor/symfony/doctrine-bridge/Form/Type/DoctrineType.php:130
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:766
/var/www/project/vendor/symfony/options-resolver/OptionsResolver.php:698
/var/www/project/vendor/symfony/form/ResolvedFormType.php:95
/var/www/project/vendor/symfony/form/FormFactory.php:76
/var/www/project/vendor/symfony/form/FormBuilder.php:97
/var/www/project/vendor/symfony/form/FormBuilder.php:256
/var/www/project/vendor/symfony/form/FormBuilder.php:206
/var/www/project/vendor/symfony/form/FormFactory.php:30
/var/www/project/tests/Unit/Form/ProductFormTest.php:86
ERRORS! Tests: 1, Assertions: 0, Errors: 1.
This error started after mocking the ManagerRegistry class. It seems that in this unit test there is no mapping for doctrine entities present.
Is there a clean way to test a form with "Symfony\Bridge\Doctrine\Form\Type\EntityType" fields?
src\App\Entity\Product.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\Supplier;
/**
* Product Entity
*
* #ORM\Entity(repositoryClass = "App\Repository\ProductRepository")
* #ORM\Table(name = "product")
*/
class Product
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct();
$this->setType(AbstractProduct::TYPE_PARENT);
}
/**
* To String
*
* #return string
*/
public function __toString()
{
return "[" . $this->id . "] Product: " . $this->ean . " | " . $this->name;
}
/**
* ID
*
* #var integer
*
* #ORM\Id
* #ORM\Column(name = "product_id", type = "integer")
* #ORM\GeneratedValue(strategy = "AUTO")
*/
protected $id;
/**
* EAN (European Article Number)
*
* #var string
*
* #ORM\Column(name = "product_ean", type = "string", length = 13)
*/
protected $ean;
/**
* Name
*
* #var string
*
* #ORM\Column(name = "product_name", type = "string", length = 128)
*/
protected $name;
/**
* Description
*
* #var string
*
* #ORM\Column(name = "product_description", type = "text", nullable = true)
*/
protected $description;
/**
* Supplier
*
* Many Products have one Supplier
*
* #var Supplier
*
* #ORM\ManyToOne(targetEntity = "Supplier", inversedBy = "products")
* #ORM\JoinColumn(name = "supplier_id", referencedColumnName = "supplier_id")
*/
protected $supplier;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set ean
*
* #param string $ean
*
* #return AbstractProduct
*/
public function setEan($ean)
{
$this->ean = $ean;
return $this;
}
/**
* Get ean
*
* #return string
*/
public function getEan()
{
return $this->ean;
}
/**
* Set name
*
* #param string $name
*
* #return AbstractProduct
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set description
*
* #param string $description
*
* #return AbstractProduct
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set supplier
*
* #param \App\Entity\Supplier $supplier
*
* #return Product
*/
public function setSupplier(Supplier $supplier = null)
{
$this->supplier = $supplier;
return $this;
}
/**
* Get supplier
*
* #return \App\Entity\Supplier
*/
public function getSupplier()
{
return $this->supplier;
}
}
src\App\Form\ProductForm.php
namespace App\Form;
use App\Entity\Supplier;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class ProductForm extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$translationDomain = "product";
/*
* Card
*/
$builder->add("ean", TextType::class, [
"label" => "product.ean",
"required" => true,
"translation_domain" => $translationDomain,
]);
$builder->add("name", TextType::class, [
"label" => "product.name",
"required" => true,
"translation_domain" => $translationDomain,
]);
$builder->add("supplier", EntityType::class, [
"class" => Supplier::class,
"choice_label" => "name",
"label" => "supplier.name",
"required" => false,
"translation_domain" => "supplier",
]);
}
}
tests\Unit\Form\ProductFormTest.php
namespace App\Tests\Unit\Form;
use App\Entity\Product;
use App\Form\ProductForm;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\ManagerRegistry;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ProductFormTest extends TypeTestCase
{
/**
* #var ManagerRegistry
*/
private $_managerRegistry;
/**
* {#inheritdoc}
*/
protected function setUp()
{
$this->_managerRegistry = $this->createMock(ManagerRegistry::class);
parent::setUp();
}
/**
* {#inheritdoc}
*/
protected function tearDown()
{
$this->_managerRegistry = null;
parent::tearDown();
}
/**
* {#inheritdoc}
*/
protected function getExtensions()
{
$entityType = new EntityType($this->_managerRegistry);
return [
new PreloadedExtension([$entityType], [])
];
}
/**
* #test
*/
public function formSubmitsValidData()
{
$createdAt = new \DateTime();
$formData = [
"ean" => "8718923400440",
"name" => "Plumbus",
"description" => "This is a household device so common it does not need an introduction",
];
$productComparedToForm = new Product();
$productComparedToForm
->setEan($formData["ean"])
->setName($formData["name"])
;
$productHandledByForm = new Product();
$form = $this->factory->create(ProductForm::class, $productHandledByForm);
$form->submit($formData);
static::assertTrue($form->isSynchronized());
static::assertEquals($productComparedToForm, $productHandledByForm);
$view = $form->createView();
foreach (array_keys($formData) as $key) {
static::assertArrayHasKey($key, $view->children);
}
}
}
First your test case should extends from Symfony\Component\Form\Test\TypeTestCase.
Then your test should look like this:
// Example heavily inspired by EntityTypeTest inside the Symfony Bridge
class ProductTypeTest extends TypeTestCase
{
/**
* #var EntityManager
*/
private $em;
/**
* #var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry
*/
private $emRegistry;
protected function setUp()
{
$this->em = DoctrineTestHelper::createTestEntityManager();
$this->emRegistry = $this->createRegistryMock('default', $this->em);
parent::setUp();
$schemaTool = new SchemaTool($this->em);
// This is the important part for you !
$classes = [$this->em->getClassMetadata(Supplier::class)];
try {
$schemaTool->dropSchema($classes);
} catch (\Exception $e) {
}
try {
$schemaTool->createSchema($classes);
} catch (\Exception $e) {
}
}
protected function createRegistryMock($name, $em)
{
$registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock();
$registry->expects($this->any())
->method('getManager')
->with($this->equalTo($name))
->will($this->returnValue($em));
return $registry;
}
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new DoctrineOrmExtension($this->emRegistry),
));
}
protected function tearDown()
{
parent::tearDown();
$this->em = null;
$this->emRegistry = null;
}
}
another solution if the one above does not work. Example on another application.
namespace App\Tests\Form;
use App\Entity\BusinessDepartment;
use App\Entity\Contact;
use App\Form\ContactForm;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ContactFormTest extends TypeTestCase
{
protected function getExtensions() {
$mockEntityManager = $this->createMock(EntityManager::class);
$mockEntityManager->method('getClassMetadata')
->willReturn(new ClassMetadata(BusinessDepartment::class))
;
$execute = $this->createMock(AbstractQuery::class);
$execute->method('execute')
->willReturn([]);
$query = $this->createMock(QueryBuilder::class);
$query->method('getQuery')
->willReturn($execute);
$entityRepository = $this->createMock(EntityRepository::class);
$entityRepository->method('createQueryBuilder')
->willReturn($query)
;
$mockEntityManager->method('getRepository')->willReturn($entityRepository);
$mockRegistry = $this->createMock(ManagerRegistry::class);
$mockRegistry->method('getManagerForClass')
->willReturn($mockEntityManager)
;
return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]);
}
public function testBuildForm()
{
$data = [
'name' => 'nameTest',
'firstName' => 'firstnameTest',
'email' => 'test_email#gmail.com',
'message' => 'messageTest'
];
$contact = new Contact();
$form = $this->factory->create( ContactForm::class, $contact);
$contactToCompare = new Contact();
$contactToCompare->setName($data['name']);
$contactToCompare->setFirstName($data['firstName']);
$contactToCompare->setEmail($data['email']);
$contactToCompare->setMessage($data['message']);
//check the submission
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($contact->getName(), $contactToCompare->getName());
$this->assertEquals($contact->getFirstName(), $contactToCompare->getFirstName());
$this->assertEquals($contact->getEmail(), $contactToCompare->getEmail());
$this->assertEquals($contact->getMessage(), $contactToCompare->getMessage());
}
}
another solution if the one above does not work. Example on another application.
ContactFromTest.php
namespace App\Tests\Form;
use App\Entity\BusinessDepartment;
use App\Entity\Contact;
use App\Form\ContactForm;
use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Component\Form\Test\TypeTestCase;
class ContactFormTest extends TypeTestCase
{
protected function getExtensions() {
$mockEntityManager = $this->createMock(EntityManager::class);
$mockEntityManager->method('getClassMetadata')
->willReturn(new ClassMetadata(BusinessDepartment::class))
;
$execute = $this->createMock(AbstractQuery::class);
$execute->method('execute')
->willReturn([]);
$query = $this->createMock(QueryBuilder::class);
$query->method('getQuery')
->willReturn($execute);
$entityRepository = $this->createMock(EntityRepository::class);
$entityRepository->method('createQueryBuilder')
->willReturn($query)
;
$mockEntityManager->method('getRepository')->willReturn($entityRepository);
$mockRegistry = $this->createMock(ManagerRegistry::class);
$mockRegistry->method('getManagerForClass')
->willReturn($mockEntityManager)
;
return array_merge(parent::getExtensions(), [new DoctrineOrmExtension($mockRegistry)]);
}
public function testBuildForm()
{
$data = [
'name' => 'nameTest',
'firstName' => 'firstnameTest',
'email' => 'test_email#gmail.com',
'message' => 'messageTest'
];
$contact = new Contact();
$form = $this->factory->create( ContactForm::class, $contact);
$contactToCompare = new Contact();
$contactToCompare->setName($data['name']);
$contactToCompare->setFirstName($data['firstName']);
$contactToCompare->setEmail($data['email']);
$contactToCompare->setMessage($data['message']);
//check the submission
$form->submit($data);
$this->assertTrue($form->isSynchronized());
$this->assertEquals($contact->getName(), $contactToCompare->getName());
$this->assertEquals($contact->getFirstName(), $contactToCompare->getFirstName());
$this->assertEquals($contact->getEmail(), $contactToCompare->getEmail());
$this->assertEquals($contact->getMessage(), $contactToCompare->getMessage());
}
}
ContacForm.php
class ContactForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name',TextType::class, [
'label' => 'Nom'
])
->add('firstName',TextType::class, [
'label' => 'Prénom'
])
->add('email', EmailType::class, [
'label' => 'Email'
])
->add('businessDepartment', EntityType::class, [
'label' => 'Département à contacter',
'class' => BusinessDepartment::class,
'choice_value' => 'id',
'choice_label' => 'nameDepartment',
])
->add('message', TextareaType::class, [
'label' => 'Votre message'
])
;
}
}
I want to create form, that serve for adding, editing (and removing when url field is empty) menu items. Problem is that the count of rows/items are variable. (As you can see on the first picture)
Questions:
1)How to write a form that has variable number of fields.
2)How to parse data into the fields at this form.
class GalleryType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
//...some textType, fileType fields,...etc
);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
//...
//some data/data_class option that parse data into the field
]);
}
Extra Information:
I am working on own simple content management system with Symfony 3 framework. I want to allow user to add menu item with information like: URL, Title and for instance FA icon, background image,..etc .
-There is always one empty row for adding item and the rest of fields are fulfilled with existing data (menu item/s). When you confirm the form, this row is added into the form (and empty row as well).
-There are few different kind of menu: main menu, slider, side menu, that has diferent type of fields. (you can see it on the second picture)
-Main menu has: title, url and some item can have children items (as sub menu)
-Slider has: title, url, color of title, background image
-Side menu has: title, url and Font Awesome Icon
I have already done form for navigation menu (footer) where is just 2 fields(title and link), but I feel this is not propertly way how to programming it... for illustrative purposes here is how I've done navigation
Controller:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\SMBundle\Navigation;
use AppBundle\Entity\Sett;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class SettingsController extends Controller {
//....
/**
* #Route("/admin/menu/navigation", name="navigation")
*/
public function navigationAction(Request $request) {
$set = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName('navigation');
$navigation = $this->deserializeFromStringToObject('navigation');
if (!$navigation) {
$set = new Sett();
$navigation = new Navigation();
}
$form = $this->createFormFromArray($navigation->getLinksArray());
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$set->setEditedAt(new \DateTime());
$set->setName('navigation');
$this->brutalHack($navigation, $form);
$set->setContent($this->serializeFromObjectToString($navigation));
// Save
$this->save($set);
return $this->redirect($this->generateUrl('navigation'));
}
return $this->render("viewSM/menu/navigation.html.twig", array('form' => $form->createView()));
}
private function deserializeFromStringToObject($name) {
$object = $this->getDoctrine()->getRepository('AppBundle:Sett')->findOneByName($name);
if (!$object) {
return null;
}
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->deserialize($object->getContent(), 'AppBundle\\Entity\\SMBundle\\' . ucfirst($name), 'json');
}
private function serializeFromObjectToString($object) {
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new JsonEncoder()));
return $serializer->serialize($object, 'json');
}
private function createFormFromArray(array $collection) {
$i = 0;
$formBuilder = $this->createFormBuilder();
foreach ($collection as $key => $value) {
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'data' => '' . $key, 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'data' => '' . $value, 'attr' => ['class' => 'form-control']]);
$i++;
}
$formBuilder
->add('url' . $i, TextType::class, ['label' => 'URL ', 'attr' => ['class' => 'form-control']])
->add('name' . $i, TextType::class, ['label' => 'Titulek ', 'attr' => ['class' => 'form-control']])
->add('submit', \Symfony\Component\Form\Extension\Core\Type\SubmitType::class, ['label' => 'Uložit', 'attr' => ['class' => 'btn btn-primary']]);
$form = $formBuilder->getForm();
return $form;
}
private function save($set) {
$em = $this->getDoctrine()->getManager();
$em->persist($set);
$em->flush();
}
private function brutalHack($navigation, $form) {
$nav = array();
if (count($navigation->getLinksArray()) == 0) {
$nav[$form['url0']->getData()] = $form['name0']->getData();
}
for ($i = 0; $i < count($navigation->getLinksArray()); $i++) {
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
}
$key = $form['url' . $i]->getData();
$value = $form['name' . $i]->getData();
if ($key != NULL && $value != NULL) {
$nav[$key] = $value;
}
$navigation->setLinksArray($nav);
}
//...
}
Entity:
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="sett")
* #ORM\HasLifecycleCallbacks
*/
class Sett
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", length=255)
*/
private $name;
/**
* #ORM\Column(name="content", type="json_array")
*/
private $content;
/**
* #ORM\Column(name="edited_at", type="datetime")
*/
private $editedAt;
/**
* #ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/**
* #ORM\PrePersist
*/
public function onPrePersist()
{
$this->createdAt = new \DateTime();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Set
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set content
*
* #param array $content
*
* #return Set
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* #return array
*/
public function getContent()
{
return $this->content;
}
/**
* Set editedAt
*
* #param \DateTime $editedAt
*
* #return Set
*/
public function setEditedAt($editedAt)
{
$this->editedAt = $editedAt;
return $this;
}
/**
* Get editedAt
*
* #return \DateTime
*/
public function getEditedAt()
{
return $this->editedAt;
}
/**
* Set createdAt
*
* #param \DateTime $createdAt
*
* #return Set
*/
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt
*
* #return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
Data class:
class Navigation
{
private $linksArray;
public function __construct() {
$this->linksArray=array();
}
function getLinksArray() {
return $this->linksArray;
}
function setLinksArray($linksArray) {
$this->linksArray = $linksArray;
}
function add($key,$value){
$this->linksArray[$key]=$value;
}
}
I am not sure if this will work but you should give it a try.
2)How to parse data into the form that has variable number of fields.
You can send the data as form $options.
in your controller
$oForm = $this->createForm(YourFormType::class,
$FormObject, [
'your_options' => [
'Checkbox' => 'FieldName1',
'TextArea' => 'FieldName2'
]);
in your form
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach($options['your_options'] as $key, $option) { //you can name $option as $filedName or whatever you find convenient
$builder->add($option, $key.Type::class);
}
...}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'your_options' => null
])
}
I have 3 tables:
Category ( id, name)
Category_Tournament (category_id, tournament_id) --> pivot table
Category_Tournament_User (category_id, tournament_id, user_id, confirmed)
Category is the list of available categories
Category_Tournament is the list of categories the admin configured
Category_tournament_User is the categories the user has registred
To get all categories in the tournament, I can do it easily with:
tournament->categories
defining a belongsToMany relationship in tournament model
What I don't know how to define relationship with the last table.
What I need is the user click on several categories, and I can run something like:
tournament->user_categories->sync($userCategories)
where I should sync table Category_Tournament_User ( with category_id, tournament_id, user_id)
What is the best way to achieve it???
EDIT:
Model Tournament:
class Tournament extends Model
{
protected $table = 'tournament';
public $timestamps = true;
protected $fillable = [
'name',
'date',
'type',
];
/**
* A tournament is owned by a user
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
return $this->belongsTo('App\User', 'user_id','id');
}
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function categories()
{
return $this->belongsToMany('App\Category')
->withTimestamps();
}
}
Model Category
class Category extends Model
{
protected $table = 'category';
public $timestamps = true;
protected $fillable = [
'id',
'name',
];
public function tournaments()
{
return $this->belongsToMany('App\Tournament');
}
}
Model User:
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, HasRole;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name','firstname','lastname','email', 'password','avatar',country_id','role_id',,'provider','provider_id','verified'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
/**
* Boot the model.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::creating(function ($user) {
$user->token = str_random(30);
});
}
public function role()
{
return $this->belongsTo('App\Role');
}
public function settings()
{
return $this->hasOne('App\Settings');
}
public function invites()
{
return $this->hasMany('App\Invite', 'email','email');
}
public function country()
{
return $this->belongsTo('Webpatser\Countries\Countries');
}
/**
* A user can have many tournaments
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function tournaments()
{
return $this->hasMany('App\Tournament');
}
}
You have many to many relationship here between User and Category_Tournament and you should take a look in documentation exactly at Many To Many.
I think you don't need to to have Category_Tournament_User table. and you can't make a Model for it in Laravel. you only need to a table user_tournament. and you should define relation(foreign key) on migration, like this:
Schema::create('user_tournament', function(Blueprint $table){
$table->engine = 'InnoDB';
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('tournament_id')->unsigned();
$table->unique(['tournament_id', 'user_id']);//You can omit this
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('tournament_id')->references('id')->on('tournaments')->onDelete('cascade')->onUpdate('cascade');
$table->nullableTimestamps();
});
then you can use this code:
user->tournaments->sync($userCategories);
I have an Entity containing Self-Referenced mapping.
class Category
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=100)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="Category", mappedBy="parent")
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
}
In my CategoryType I have this :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plan = $this->plan;
$builder->add('name');
$builder->add('parent', 'entity', array(
'class' => 'xxxBundle:Category',
'property' => 'name',
'empty_value' => 'Choose a parent category',
'required' => false,
'query_builder' => function(EntityRepository $er) use ($plan) {
return $er->createQueryBuilder('u')
->where('u.plan = :plan')
->setParameter('plan', $plan)
->orderBy('u.id', 'ASC');
},
));
}
Actually, when I render the form field Category this is something like
Cat1
Cat2
Cat3
Subcat1
Subcat2
Cat4
I would like to know if it's possible and how to display something more like, a kind of a simple tree representation :
Cat1
Cat2
Cat3
-- Subcat1
-- Subcat2
Cat4
Regards.
I came up with something which seems correct from what you and jperovic wrote. You will need two new attributes for your Category class :
$level will contain ID's of its parents like "idA-idB", etc. this attribute will be use to sort your results when querying your database so you can be certain SubCatOf3 won't come before Cat3 !
$treeName will contain what jperovic already wrote and will be printed in the form.
I also used Doctrine Events [doc] so when you update/persist them, you don't have to worry about the value of these attributes.
This is your brand new Category.php file :
/**
* #ORM\HasLifeCycleCallbacks()
*/
class Category
{
private $level;
private $treeName;
/**
* Renders something like : "---- Subcategory A"
* #ORM\PreUpdate()
* #ORM\PrePersist()
**/
public function updateTreeName()
{
$itemDepth = 0;
$parent = $this->parent;
while ($parent != null) {
$itemDepth++;
$parent = $parent->getParent();
}
$this->treeName = str_repeat('--', $itemDepth) . ' ' . $this->name
}
/** renders something like : "idParent-idChild1-idChild2"
* #ORM\PreUpdate()
* #ORM\PrePersist()
**/
public function updateLevelName()
{
$this->level = '';
$parent = $this->parent;
while ($parent != null) {
$parent = $parent->getParent();
$this->level .= '-' . $p->getId();
}
}
public function getTreeName()
{
return $this->treeName;
}
public function getLevel()
{
return $this->level;
}
// ...
}
Then, put your query_builder in your CategoryRepository.php like this :
namespace Foo\BarBundle\Entity;
use Doctrine\ORM\EntityRepository;
class CategoryRepository extends EntityRepository
{
public function getHierarchicalCategoryList($plan)
{
$qb = $this->createQueryBuilder('u')
->where('u.plan = :plan')
->setParameter('plan', $plan)
->orderBy('u.level', 'ASC');
$return $qb;
}
}
And in your CategoryType.php :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plan = $this->plan;
$builder->add('name');
$builder->add('parent', 'entity', array(
'class' => 'xxxBundle:Category',
'property' => 'treeName',
'empty_value' => 'Choose a parent category',
'required' => false,
'query_builder' => function(EntityRepository $er) use ($plan) {
return $er->getHierarchicalCategoryList($plan)
},
));
}
Note : this is quick&dirty work so you might need to correct typos, annotations, etc. Yet, you have the idea ! Hope it helps.
This is a really long shot at best, but I think it could be achieved pretty easily.
Within your query_builder you specified the 'property' => 'name'. You would need to change it to 'treeName'. Doctrine will try to find and invoke property's getter method - that's where all the printing logic comes in:
class Category
{
....
Everything else
....
public function getTreeName(){
$itemDepth = 0;
$p = $this->parent;
while ( $p != null ){
$itemDepth++;
$p = $p->getParent();
}
return str_repeat('--', $itemDepth) . ' ' . $this->name
}
}
This could pose a serious performance hit due to need to iterate for each item the depth times.
What do you think? What is the average depth of items?
Just to be clear, name property and its getter and setter are to remain intact.