I want to "empty" an ObjectStorage when updating a Object:
It's TYPO3 4.6 with a Extbase Extension which allows you to show/add/edit/delete datasets in the frontend. At first sight everything looks good.
I have one field referencing another table:
TCA:
'partner' => array(
'exclude' => 0,
'label' => 'LLL:EXT:toco3_marketingdb/Resources/Private/Language/locallang_db.xlf:tx_toco3marketingdb_domain_model_firma.partner',
'config' => array(
'type' => 'select',
'size' => 5,
'foreign_table' => 'tx_toco3marketingdb_domain_model_partner',
'foreign_table_where' => 'ORDER BY tx_toco3marketingdb_domain_model_partner.partnerpkey',
'minitems' => 0,
'maxitems' => 20,
),
),
Model:
/**
* Partner
*
* #var Tx_Extbase_Persistence_ObjectStorage<Tx_Toco3Marketingdb_Domain_Model_Partner>
* #lazy
*/
protected $partner;
/**
* Sets the partner
*
* #param Tx_Extbase_Persistence_ObjectStorage<Tx_Toco3Marketingdb_Domain_Model_Partner> $partner
* #return void
*/
public function setPartner(Tx_Extbase_Persistence_ObjectStorage $partner) {
$this->partner = $partner;
}
Controller:
$partner = new Tx_Extbase_Persistence_ObjectStorage();
if (count($partnerarr) > 0){
foreach($partnerarr as $p){
$partner->attach( $this->partnerRepository->findByUid($p));
}
}
$organisation = $this->organisationRepository->findByUid($uid)
$organisation->setPartner($partner);
This is working as long there is an Object in the ObjectStorage. So I can add/delete/change relations. But when $partnerarr is empty an no objects get attached an empty Tx_Extbase_Persistence_ObjectStorage is assigned, the old values do not get "deleted". I also tried to assign null or "" but the an error occures because an ObjectStorage is needed. If I assign the empty ObjectStorage I don't get an error, but the old values still maintain :(
Any idea?
Thank you
Christian
Call the detach or removeAll methods to remove certain or all objects of the storage.
/** #var \Tx_Extbase_Persistence_ObjectStorage $organisationPartners */
$organisationPartners = $organisation->getPartner();
foreach ($organisationPartners as $partner) {
$organisationPartners->detach($partner);
}
Thank you #Wolfgang for your message.
I added the following function to my model:
/**
* detach Partner
*
* #param Tx_Toco3Marketingdb_Domain_Model_Partner $partner
* #return void
*/
public function detachPartner($partner) {
$this->partner->detach($partner);
}
In the controller I added:
$persistanceManager = t3lib_div::makeInstance('Tx_Extbase_Persistence_Manager');
$organisation = $this->firmaRepository->findByUid($uid);
$organisationPartners = $organisation->getPartner();
foreach ($organisationPartners as $organisationPartner) {
$organisation->detachPartner($organisationPartner);
}
$persistanceManager->persistAll();
$organisation->setPartner($partner);
It is important to persist before setting the new (empty) value...
Related
I followed the documentation and was able to add custom constraints on many of my fields (http://symfony.com/doc/current/validation/custom_constraint.html).
I'm figuring a problem with CollectionType field.
My custom constraint just check if user didn't tap multiple space in field (the constraint doesn't matter anyway).
I have a Question form with a title and answers :
$builder
->add('title', TextType::class)
->add('answers', CollectionType::class, array(
'entry_type' => AnswerType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
))
I have my constraint :
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class ContainsText extends Constraint
{
public $message = 'constraint_error';
}
And my constraint validator :
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ContainsTextValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
// It checks if user didn't had multiple space in field
if (strlen(trim($value)) == 0) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
In my entities :
Question:
use XX\XXBundle\Validator\Constraints as CustomAssert;
class Question
{
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255, unique=true)
* #CustomAssert\ContainsText
*/
private $title;
...
}
Answer :
use XX\XXBundle\Validator\Constraints as CustomAssert;
class Answer
{
/**
* #var string
*
* #ORM\Column(name="text", type="string", length=255, unique=true)
* #CustomAssert\ContainsText
*/
private $text;
...
}
In my form validation, if in Question title I tap many spaces, I get a form validation error with my "constraint_error" message => Everything is working.
But, if in Question answers text I tap many spaces, the form validation doesn't return any errors and my question is created with empty answers !
It seems that, if the field comes from a CollectionType, the custom asserts are ignored.
What I don't understand is, if i had a Assert (like #Assert\Blank(), not a custom one) on answer text, even if we are in a CollectionType, the assert is not ignored and I can't validate a form with a blank answer.
What did I miss here ? TY
Not sure which Symfony 2 version you use, but depending if that is pre 2.8 or later you have different ways to tackle this:
v2.8+ and v3.0+
Starting with v2.8, which I suspect you could be using given AnswerType::class, cascade_validation was deprecated. Instead, you need to applty Valid constraint, on you Question::$answers class member. Something like this:
class Question
{
/**
* ... Other anotaions go here
*
* #Assert\Valid()
*/
private $answers
}
Pre v2.8:
You need to specify cascade_validation option:
$builder
->add('title', TextType::class)
->add('answers', CollectionType::class, array(
'entry_type' => AnswerType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'cascade_validation' => true // <========= THIS
));
Hope this helps...
Is it possible to inverse a 1:1 Relation without adding a second field into DB in Extbase?
Example:
The extension has Contact-Persons which can have an fe_user.
The Contact-Person-Domain-Model is the owning site of the relation.
Now you can use $contactPerson->getFrontendUser();
Is there any way to add an inversed property to FrontendUser without adding it to the DB?
So you can use $frontendUser->getContactPerson(),
or even more important: $frontendUserRepository->findByContactPerson();
I tried adding the property to the FrontendUser-Model:
/**
* FrontendUser
*/
class FrontendUser extends \TYPO3\CMS\Extbase\Domain\Model\FrontendUser
{
/**
* #var \Vendor\ExtKey\Domain\Model\ContactPerson
*/
protected $contactPerson = null;
}
And overriding the fe_users TCA:
$GLOBALS['TCA']['fe_users']['columns']['contact_person'] = array(
'exclude' => 1,
'label' => 'LLL:EXT:ExtKey/Resources/Private/Language/locallang_db.xlf:tx_ExtKey_domain_model_contactperson',
'config' => array(
'type' => 'inline',
'foreign_table' => 'tx_ExtKey_domain_model_contactperson',
'foreign_field' => 'frontend_user',
'minitems' => 0,
'maxitems' => 1,
),
);
But when I call:
/**
* The repository for Customers
*/
class FrontendUserRepository extends \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository
{
/**
* #param \Vendor\ExtKey\Domain\Model\ContactPerson $contactPerson
* #param boolean $ignoreEnableFields
* #param boolean $respectStoragePage
* #return object
*/
public function findByContactPerson(ContactPerson $contactPerson, $ignoreEnableFields = false, $respectStoragePage = true){
$query = $this->createQuery();
$query->getQuerySettings()
->setIgnoreEnableFields($ignoreEnableFields)
->setRespectStoragePage($respectStoragePage);
$query->matching($query->equals('contactPerson', $contactPerson));
return $query->execute()->getFirst();
}
}
It creates following SQL-Error:
Unknown column 'fe_users.contact_person' in 'where clause'
Computed bidirectional 1:1 relations are not supported in TYPO3, only m:n relations support that with an extra definition in TCA - which also requires an additional field on the opposite site of the relation.
Concerning you scenario, you have to create the additional property and database field in an extended FrontendUser domain model on your own.
There is a possibility like described here: http://www.oliver-weiss.com/blog/einzelansicht/article/relationen-in-typo3-teil-1-11-relation/News/detail/ . Additionally, instead of having an "inline" relation on both sides, this also works if only one side is "inline" and the other side is "select". But the single (inverted) "inline" relation needs to have "foreign_field" defined. Following this, the "foreign" uid is only stored on one side of the relation.
I have Company and Number entity which are related
/**
* #var Comapany
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="numbers", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="company", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
* #Assert\NotBlank(groups={"client"})
* #Assert\Valid()
*/
private $company;
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
I have created a form for creating and updating Company entity. This form should allow to set Number entities to it as well as unset them. This is how it looks rendered
And this is how it looks in code:
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
The problem is when creating new Company record, the form assigns Number entities, but the Number entities have property "company" which doesn't get assigned and so no relation is made. I have worked around this with form events:
$builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
foreach ($event->getData()->getNumbers() as $number) {
$number->setCompany($event->getData());
}
});
Which works for creating record, however when updating I have another issue, since I remove Number associations I have no access to them and thus can't update them in database. I could again select all Number entities assigned to form, and then filter out which were assigned to company and which were not and then manually update them, but this feels dirty and I would like to work it out in a clean way.
Finally found solution, as is turns out it's quite well documented:
http://symfony.com/doc/current/cookbook/form/form_collections.html
The changes I Had to make was:
1.Add by_reference property to entity form field: More information on that with an example: http://symfony.com/doc/current/cookbook/form/form_collections.html#allowing-new-tags-with-the-prototype
Basically what I figured that without this options symfony2 form uses own means of adding associations, and with this option set it calls methods "addNumber" and "removeNumber" inside Entity in which I had to manually add inverse side "number" association which goes to 2nd change I had to make.
$builder
->add('name', 'text', [
'required' => false
])
->add('numbers', 'entity', [
'class' => 'AppBundle:Number',
'property' => 'number',
'placeholder' => '',
'required' => false,
'multiple' => true,
'by_reference' => false, //
'query_builder' => function (EntityRepository $er) use ($builder) {
if ($builder->getData() && $id = $builder->getData()->getId()) {
return $er->createQueryBuilder('n')
->where('n.company is NULL')
->orWhere('n.company = :id')
->setParameter('id', $id);
}
return $er->createQueryBuilder('n')
->where('n.company is NULL');
}
]);
2.I had explicitly set Inverse side association to owning side by calling method setComapany($this) from owning (Company Entity) side.
/**
* Add numbers
*
* #param \AppBundle\Entity\Number $numbers
* #return Company
*/
public function addNumber(\AppBundle\Entity\Number $numbers)
{
$numbers->setCompany($this); //!Important manually set association
$this->numbers[] = $numbers;
return $this;
}
These 2 changes are enough to make form automatically add associations. However with removing associations there's a little bit more.
3.Change I had to make to correctly unset associations was inside controller action itself: I had to save currently set associations inside new ArrayCollection variable, and after form validation manually go through each item in that collection checking if it exists after form was validated. Important note:
I had also manually unset inverse side association to owning side by calling:
"$number->setCompany(null);"
public function editAction(Request $request, Company $company)
{
$originalNumbers = new ArrayCollection();
foreach ($company->getNumbers() as $number) {
$originalNumbers->add($number);
}
$form = $this->createForm(new CompanyType(), $company);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
foreach ($originalNumbers as $number) {
if (false === $company->getNumbers()->contains($number)) {
$company->getNumbers()->removeElement($number);
$number->setCompany(null); //!Important manually unset association
}
}
$em->persist($company);
$em->flush();
return $this->redirectToRoute('companies');
}
return $this->render('AppBundle:Company:form.html.twig', [
'form' => $form->createView()
]);
}
All of these steps are required to make this kind of logic function properly, luckily I was able to really good documentation for that.
PS. Note that I call
$em->persist($company);
$em->flush();
without persisting each "Number" Entity iterated inside loop which you can be seen in given Symfony2 documentation example, and which in this case would look like this:
$company->getNumbers()->removeElement($number);
$number->setCompany(null);
$em->persist($number);
This is because I setup Cascading Relations options inside my Entity class
/**
* #var Number[]
* #ORM\OneToMany(targetEntity="Number", mappedBy="company", fetch="EXTRA_LAZY", cascade={"persist", "remove"})
* #Assert\Count(min="1")
*/
private $numbers;
My advise for anyone struggling with this is to read whole http://symfony.com/doc/current/cookbook/form/form_collections.html thoroughly especially sections marked by special signs ✎✚❗☀💡
Domain model
class Image extends AbstractContent {
/**
* #var \TYPO3\CMS\Extbase\Domain\Model\FileReference
*/
protected $file;
/**
* Gets the image file
*
* #return \TYPO3\CMS\Extbase\Domain\Model\FileReference
*/
public function getFile() {
return $this->file;
}
/**
* Sets the image file
*
* #param \TYPO3\CMS\Extbase\Domain\Model\FileReference $file
* #return void
*/
public function setFile($file) {
$this->file = $file;
}
}
Import service fragments
/**
* #var \TYPO3\CMS\Core\Resource\ResourceStorage
*/
protected $defaultStorage;
[...]
$this->defaultStorage = ResourceFactory::getInstance()->getDefaultStorage();
[...]
$file = $this->defaultStorage->addFile(
'/tmp/4711',
$this->defaultStorage->getRootLevelFolder(),
'foo.jpg',
'overrideExistingFile'
);
$falReference = ResourceFactory::getInstance()->createFileReferenceObject(
array(
'uid_local' => $file->getUid(),
'uid_foreign' => uniqid('NEW_'),
'uid' => uniqid('NEW_'),
)
);
$reference = GeneralUtility::makeInstance(FileReference::class);
$reference->setOriginalResource($falReference);
$content = GeneralUtility::makeInstance(Image::class);
$content->setFile($reference);
After saving $content the image is available through the record and the filemount but the Ref column in BE > FILE > File List) is - and not >= 1. So its look like the reference is some how broken. When I'm using the BE to add an image to the record it's all fine. I'm using TYPO3 CMS 7.3-dev.
What's wrong with my code?
I get the hint in the Slack channel of TYPO3.
You just need to set plugin.tx_myext.persistence.updateReferenceIndex = 1 respectively module.tx_myext.persistence.updateReferenceIndex = 1 and the index will be updated.
Alternatively you could use \TYPO3\CMS\Core\Database\ReferenceIndex::updateRefIndexTable().
When I had to use FAL in my extension I found this link:
http://t3-developer.com/extbase-fluid/extensions-erweitern/fal-in-eigenen-extensions/fal-in-typo3-extensions-verwenden/
Since it is in German, I will in the very shortest explain what is done there:
extend your data model in ext_tables.sql
add a column of some char type (e.g. varchar)
add your column to the column section in your TCA array in ext_tables.php
'mypictures' => array(
'exclude' => 1,
'label' => 'My Pictures',
'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig('image', array(
'appearance' => array(
'createNewRelationLinkTitle' => 'LLL:EXT:cms/locallang_ttc.xlf:images.addFileReference'
),
'minitems' => 0,
'maxitems' => 99,
), $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']),
),
Extend your modelfiles. Pay attention to annotations!
You can use your media in your fluid template
I was just extending an existing Typo3 4.7 Extension with two own Model classes.
It runs quite well, Backendforms look like expected BUT when I try to access some SubObjects of my Model Classes in the templates via Object Accessor {class.subclass.attribute} I am not able to access the attribute. The problem that showed me, is that the Object for the Attribute "mainColor" for example in the Object Storage is a HashCode, which contains the Actual Object I want to access ( the object following the hashcode is the correct related object from the database ).
Does anyone of you have an Idea where the problem might be ?
If any more Code Snippets are needed, I will deliver them. But since I really don't know where the problem comes from, I prefer to not deliver a wall of Code.
Domain/Model/Cluster.php
/**
* Main Color of Cluster
* #var Tx_Extbase_Persistence_ObjectStorage<Tx_Alp_Domain_Model_ColorCombination> $mainColor
*/
protected $mainColor;
/**
* Subcolors of Cluster
* #var Tx_Extbase_Persistence_ObjectStorage<Tx_Alp_Domain_Model_ColorCombination> $subColors
*/
protected $subColors;
/**
* Constructor
* #return void
*/
public function __construct() {
$this->initStorageObjects();
}
/**
* Initializes all Tx_Extbase_Persistence_ObjectStorage properties.
* #return void
*/
protected function initStorageObjects() {
$this->subColors = new Tx_Extbase_Persistence_ObjectStorage();
$this->mainColor = new Tx_Extbase_Persistence_ObjectStorage();
}
TCA/Cluster.php
'sub_colors' => array(
'exclude' => 1,
'label' => 'Sub-Colors',
'config' => array(
// edited
'type' => 'inline',
'internal_type' => 'db',
'allowed' => 'tx_alp_domain_model_colorcombination',
'foreign_table' => 'tx_alp_domain_model_colorcombination',
'MM' => 'tx_alp_cluster_subcolorcombination_mm',
'MM_opposite_field' => 'parent_cluster',
'size' => 6,
'autoSizeMax' => 30,
'maxitems' => 9999,
'multiple' => 0,
'selectedListStyle' => 'width:250px;',
'wizards' => array(
'_PADDING' => 5,
'_VERTICAL' => 1,
'suggest' => array(
'type' => 'suggest',
),
),
),
),
Fluid Debug Output can be found here:
http://i60.tinypic.com/28kluub.jpg
Thanks for any help :(
And sorry for my bad English and this is my first question here, hope I did it right ;)
Unless you have a 1:1 relation from a model to a sub model, you cannot access the sub model because the sub model is an ObjectStorage.
Example:
Domain/Model/Cluster.php
/**
* Main Color of Cluster
* #var Tx_Alp_Domain_Model_ColorCombination $mainColor
*/
protected $mainColor;
This means that the Cluster model has exacly one main color (mind the annotation), this is a 1:1 relation.
Therefore using {cluster.mainColor.property} will work.
What you are doing is:
Domain/Model/Cluster.php
/**
* Main Color of Cluster
* #var Tx_Extbase_Persistence_ObjectStorage<Tx_Alp_Domain_Model_ColorCombination> $mainColor
*/
protected $mainColor;
This means that every Cluster can have multiple main colors, this is a 1:n relation. Therefore you must iterate through the mainColors (and call the property $mainColors):
<f:for each="{cluster.mainColors}" as="mainColor">
{mainColor.property}
</f:for>