Edit hidden records in frontend - typo3

I am building an extension to edit tt_news records in frontend.
I set setIgnoreEnableFields(TRUE) in my repository.
But if I try edit a hidden record, I get the error
Object with identity „12345" not found.
Any solution for this?

I am guessing you are using an action like
/**
* Single view of a news record
*
* #param \Vendor\Ext\Domain\Model\News $news news item
*/
public function detailAction(\Vendor\Ext\Domain\Model\News $news = null)
Your problem is, that the Repository is not used to fetch the record.
As a solution, remove the argument, clear the caches and try something like that
/**
* Single view of a news record
*
* #param \Vendor\Ext\Domain\Model\News $news news item
*/
public function detailAction() {
$id = (int)$this->request->getArgument('news');
if ($id) {
$news = $this->newsRepository->findByUid($previewNewsId);
}
}
Now you can manipulate the QuerySettings and use those.

The problem is the PropertyMapping. If extbase try to assign an uid (12345) to an Domain Object (tt_news) the "setEnableFields" setting of the Repository isn't respected. So you must fetch the object by yourself.
the simple solution is to do this in an initialize*Action for each "show" action. For editAction an example:
public function initializeEditAction() {
if ($this->request->hasArgument('news')) {
$newsUid = $this->request->getArgument('news');
if (!$this->newsRepository->findByUid($newsUid)) {
$defaultQuerySettings = $this->newsRepository->createQuery()->getQuerySettings();
$defaultQuerySettings->setIgnoreEnableFields(TRUE);
$this->newsRepository->setDefaultQuerySettings($defaultQuerySettings);
if ($news = $this->newsRepository->findByUid($newsUid)) {
$this->request->setArgument('news', $news);
}
}
}
}
The Hard Part is to get the object to update. As I never try this I have found an TypeConverter to fetch also hidden Records at https://gist.github.com/helhum/58a406fbb846b56a8b50
Maybe Instead to register the TypeConverter for everything (like the example in ext_localconf.php) you can try to assign it only in the initializeUpdateAction
public function initializeUpdateAction() {
if ($this->arguments->hasArgument('news')) {
$this->arguments->getArgument('news')->getPropertyMappingConfiguration()
->setTypeConverter('MyVendor\\MyExtension\\Property\\TypeConverters\\MyPersistenObjectConverter')
}
}

Related

How to store tags to custom entity?

I have custom entity and tag field defined as:
/**
* #ORM\ManyToMany(targetEntity="Sulu\Bundle\TagBundle\Tag\TagInterface")
* #ORM\JoinTable(name="venue_tags",
* joinColumns={#ORM\JoinColumn(name="venue_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="idTags", referencedColumnName="id")}
* )
*/
private $tags;
Then I have setter (adder actually) like this:
public function addTag(TagInterface $tag)
{
$this->tags[] = $tag;
return $this;
}
Field is added to form details XML like:
<property name="tags" type="tag_selection">
<meta>
<title lang="de">Tags</title>
<title lang="en">Tags</title>
</meta>
<tag name="sulu.search.field" type="tags"/>
</property>
And inside my admin controller class I have mapping method:
protected function mapDataToEntity(array $data, Venue $entity): void
{
....
foreach ($data['tags'] as $tag) {
$entity->addTag($tag);
}
....
However as $data['tags'] I receive here array of string and my addTag() requires instance of TagInterface object. How can I create that object from string I have and generally is this the proper way for storing tags.
Update:
As #Johannes suggested added field:
/**
* #var TagManagerInterface
*/
private $tagManager;
and to constructor like:
public function __construct(
ViewHandlerInterface $viewHandler,
...
TagManagerInterface $tagManager
) {
$this->viewHandler = $viewHandler;
...
$this->tagManager = $tagManager;
parent::__construct($viewHandler);
}
And then trying to add tags like this:
$tagEntities = $this->tagManager->resolveTagNames($data['tags']);
foreach ($tagEntities as $tagEntity) {
$entity->addTag($tagEntity);
}
But then I get error:
Argument 1 passed to App\Entity\Venue::addTag() must implement interface Sulu\Bundle\TagBundle\Tag\TagInterface, int given, called in /var/www/html/src/Controller/Admin/VenueController.php on line 147
What I'm doing wrong? Like I'm getting tag ids? Should I load tags somehow?
Update 2:
After adding findById() saving tags works. However, now I found 2 more problems:
Even relations are saved well in database just after saving tags are not displayed any more in tags field. Also if I go to overview page and get back to edit entity selected (saved) tags are not re-populated. What else I have to add so saved tags in database would be used actually?
If I fist select "Tag1" and click "Save" it's saved well. But if I edit entity again and select "Tag1" again Sulu tries to create new row with same keys and I get error message: "SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '4-1' for key 'venue_tags.PRIMARY'". I guess previously saved keys should be deleted first but how and where would be the optimal way?
You can use the TagManager service (id: sulu_tag.tag_manager or Sulu\Bundle\TagBundle\Tag\TagManagerInterface):
$tagIds = $tagManager->resolveTagNames($data['tags']);
foreach ($tagIds as $tagId) {
$entity->addTag($tagManager->findById($tagId));
}
For the record, I added to my entity new method:
public function removeAllTags()
{
foreach ($this->getTags() as $tag) {
$this->removeTag($tag);
}
}
which I'm calling from mapDataToEntity() before adding new tags:
$tagEntities = $this->tagManager->resolveTagNames($data['tags']);
$entity->removeAllTags();
foreach ($tagEntities as $tagEntity) {
$entity->addTag($this->tagManager->findById($tagEntity));
}
And this solves doubled keys but also solves tag removal potential issue.
Add following code to your class that should fix your first issue
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("tags")
*/
public function getTagNameArray()
{
$tags = [];
foreach ($this->getTags() as $tag) {
$tags[] = $tag->getName();
}
return $tags;
}
To fix the second one you have to make the addTag method aware that you cannot have multiple relations to the same tag (check if the tag is already in the collection there).

Extbase property mapping for deleted record

I would like to build a preview page for a create form. I set "deleted" property of the record to "1" when in previewAction because in the BE the list module is used to approve the inserted records - so if the record was never finally saved its deleted anyway.
Problem: I can create the record (deleted=1) - I can jump back to the form (no history back for I have to keep the created object). But if I submit again the property mapping tells me
Object of type MyModel with identity "3" not found.
Of course that's because its deleted. The settings in the Repository to ignore deleted are not taking action here.
Yes I could bypass the Extbase magic by filling up everything manually, but this is not what I want.
Here is the action to get an idea what I'm trying
/**
* action preview
*
* #param MyModel
* #return void
*/
public function previewAction(MyModel $newModel)
{
//check if model was already saved
$uid = $this->request->hasArgument('uid') ? this->request->getArgument('uid') : 0;
if($uid){
$newModel = $this->myRepository->findDeletedByUid($uid);
$this->myRepository->update($newModel);
}
else{
$newModel->setDeleted(true);
$this->myRepository->add($newModel);
}
$this->view->assign('ad', $newModel);
$this->persistenceManager->persistAll();
$uid = $this->persistenceManager->getIdentifierByObject($newModel);
$this->view->assign('uid', $uid);
}
Any ideas?
The Extbase default query settings suppress deleted objects.
Since you've already stated the custom query findDeletedByUid() in your repository, you just need to set it to include deleted records. It is important, however, that if you want to call your controller action using the object, you'll have to retrieve it before calling the action. Use an initialization action for that. The initializaton will be called automatically before the action.
If you want to set wether the object is deleted, you'll also going to need to define a property, getter and setter in your Domain Model and a proper definition in your tca to enable the data mapper to access the column.
In the repository:
public function findDeletedByUid($uid) {
$query = $this->createQuery();
$query->getQuerySettings()->setIncludeDeleted(true);
$query->matching(
$query->equals('uid',$uid)
);
return $query->execute();
}
In your Controller class:
/**
* initialize action previewAction
* Overrides the default initializeAction with one that can retrieve deleted objects
*/
public function initializePreviewAction(){
if( $this->request->hasArgument('mymodel') ){
$uid = $this->request->getArgument('mymodel');
if( $mymodel = $this->mymodelRepository->findDeletedByUid($uid) ){
$this->request->setArgument($mymodel);
} else {
// handle non retrievable object here
}
} else {
// handle missing argument here
}
}
In your Domain Model:
...
/**
* #var bool
*/
protected $deleted;
/**
* #return bool
*/
public function getDeleted() {
return $this->deleted;
}
/**
* #param bool $deleted
*/
public function setDeleted($deleted) {
$this->deleted = $deleted;
}
In your tca.php
...
'deleted' => array(
'exclude' => 1,
'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.deleted',
'config' => array(
'type' => 'check',
),
),
Instead of doing any magic with deleted, you should use the hidden field to allow editors to preview documents.
You can tell your query to include hidden records inside the repository.
Your findDeletedByUid($uid) function caught my eye. If it's not a custom function, should it use something like findByDeleted(TRUE) or findByDeleted(1) in combination with ->getFirst() or ->findByUid()? You can find discussions in the Extbase manual reference and the Repository __call() function API sections.
Thanks for all hints.
I think depending to the answers its not possible without bypass extbase property-mapping magic. So I think in general its not a good idea to do it like that.
So I put now my own flag "stored" to the model.
In BE List-Module the not "stored" objects are still visible, but using an own BE Module or deleting the not "stored" object by a cron-job should do the job.
If anyone has a bedder idea feel free to share it :-)

Extbase update deleted=1 object in Controller fails (Object with identity x not found)

I want do give the function to 'restore' deleted Object in my FE-Ext. It seems, that it does not find any deleted records an so i cannot update them set deleted = 0.
What you be you sugestion to handle that from the controller?:
$query->getQuerySettings()->setIgnoreEnableFields(TRUE);
$query->getQuerySettings()->setIncludeDeleted(TRUE);
Thank you.
Im not quite sure what you mean by "from the controller". Normally you implement this in your repository and just call the method from the controller.
In your repo:
public function findRecordEvenIfItIsDeleted($uid) {
$query = $this->createQuery();
$settings = $query->getQuerySettings();
settings->setIgnoreEnableFields(TRUE);
settings->setIncludeDeleted(TRUE);
$query->matching($query->equals('uid', $uid));
return $query->execute();
}
In your controller:
$myObject = $this->myRepsository->findRecordEvenIfItIsDeleted($uid);
Done. (Of course your storage pid must be set (or disable respectStoragePage as well)
You're adding does not throw any error because you are setting the querySettings to include deleted records. But maybe, this setting has to be enabled even when you are updating, as the repository should find the object you are updating.
I haven't tested it but give this a try.
In your repository(just a pseudo code)
public function update($modifiedObject) {
settings->setIncludeDeleted(TRUE);
parent::update($modifiedObject);
}
I know this question was asked long time ago but today i had a similar problem with a hidden object. My solution was this one:
add this to your Model (in your case exchange "hidden" by "deleted"):
/**
* #var boolean
*/
protected $hidden;
/**
* #return boolean $hidden
*/
public function getHidden() {
return $this->hidden;
}
/**
* #return boolean $hidden
*/
public function isHidden() {
return $this->getHidden();
}
/**
* #param boolean $hidden
* #return void
*/
public function setHidden($hidden) {
$this->hidden = $hidden;
}
in your repository add this function to find the deleted/hidden object:
public function findHiddenByUid($uid) {
$query = $this->createQuery();
$query->getQuerySettings()->setIgnoreEnableFields(TRUE);
$query->getQuerySettings()->setEnableFieldsToBeIgnored(array('disabled','hidden'));
return $query
->matching($query->equals('uid', $uid))
->execute()
->getFirst();
}
now in your Controller you can read the object, set the "hidden" option and update it:
$yourobject = $this->yourobjectRepository->findHiddenByUid($uid);
$yourobject->setHidden(1);
$this->yourobjectRepository->update($yourobject);
Maybe not interesting for your task but for others:
In my case i additionally had the problem posting a hidden object in a form to an action. So note that if you want to post an object by a form, it is better (or probably necessary) to first set the objects deleted/hidden option to 0.

Using Image Content Objects from tt_content in Extbase

I want to write an Extbase Backend module which needs a list of all Objects generated from tt_content with CType = 'image'.
Now I started defining a simple model
class Tx_Myextension_Domain_Model_Content extends Tx_Extbase_DomainObject_AbstractEntity
{
/**
* #var string
*/
protected $header;
/**
* #return the $header
*/
public function getHeader()
{
return $this->header;
}
/**
* #param string $header
*/
public function setHeader($header)
{
$this->header = $header;
}
}
and a Repository
class Tx_Myextension_Domain_Repository_ContentRepository extends Tx_Extbase_Persistence_Repository
{
public function initializeObject()
{
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings->setRespectStoragePage(FALSE);
$this->setDefaultQuerySettings($querySettings);
}
}
As far as I know the initializeObject method is a way to get all content elements, no matter which pid they have.
At last I tried to map my Content Class on tt_content:
plugin.tx_myextension {
persistence {
classes {
Tx_Myextension_Domain_Model_Content {
mapping {
tableName = tt_content
recordType = Tx_Myextension_Domain_Model_Content
columns {
header.mapOnProperty = header
}
}
}
}
}
}
module.tx_myextension {
persistence < plugin.tx_myextension.persistence
}
No I want to use the Repo. e.g. countAll. Unfortunately it always returns 0. Looking for the MySQL query discovers the problem:
SELECT COUNT(*)
FROM tt_content
WHERE (tt_content.CType='Tx_Myextension_Domain_Model_Content')
AND tt_content.deleted=0 AND tt_content.hidden=0
AND (tt_content.starttime<=1313073660)
AND (tt_content.endtime=0 OR tt_content.endtime>1313073660)
AND tt_content.sys_language_uid IN (0,-1)
AND tt_content.pid IN (0)
Typo 3 or Extbase or something different added all these where clauses to the query. I just want to get rid of the CType and pid clauses. As I said, I thought that the method used in the Repo leads to ignoring the pid, which is obviously not the case.
Can somebody help me? All I want is an array of Image Content Elements. Thank you in advance.
Late answer: You'll most likely want to call
query->getQuerySettings()
->setRespectEnableFields(FALSE)
->setRespectSysLanguage(FALSE);
for your query. You can disable it for all queries in your repository's initializeObject method:
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings
->setRespectStoragePage(FALSE)
->setRespectEnableFields(FALSE)
->setRespectSysLanguage(FALSE);
$this->setDefaultQuerySettings($querySettings);
See: TYPO 3 API docs
Try to remove the Node "recordType" from your Persistence Definition.

Akeneo 2.1.4 : How can I change a products' parent (used product model)

I have the requirement where upon importing I need to be able to change to products' product model. I tried to do this by changing the parent in the CSV file I'm importing, but this will show the following message:
WARNING
parent: Property "parent" cannot be modified, "new_parent_code" given.
What is the proper way to make this work? I tried 'hacking' the database by manually assigning a different parent to the product by editing the parent directly in the pim_catalog_product-table, and this seemed to work, but when editing the product unexpected results occur.
Could anyone point me in the right direction how I can change a product parent upon importing?
update:
I now came up with the following solution:
In my own bundle, I added Resources/config/updaters.yml (using DependencyInjecten Extension) with the following:
parameters:
# Rewrite parent field setter so we can allow the importer to update the parent:
pim_catalog.updater.setter.parent_field.class: Vendor\Bundle\InstallerBundle\Updater\Setter\ParentFieldSetter
And my custom ParentFieldSetter.php:
namespace Vendor\Bundle\InstallerBundle\Updater\Setter;
use Akeneo\Component\StorageUtils\Exception\ImmutablePropertyException;
use Akeneo\Component\StorageUtils\Repository\IdentifiableObjectRepositoryInterface;
/**
* Class ParentFieldSetter
*/
class ParentFieldSetter extends \Pim\Component\Catalog\Updater\Setter\ParentFieldSetter
{
/**
* #var IdentifiableObjectRepositoryInterface
*/
private $productModelRepository;
/**
* ParentFieldSetter constructor.
* #param IdentifiableObjectRepositoryInterface $productModelRepository
* #param array $supportedFields
*/
public function __construct(
IdentifiableObjectRepositoryInterface $productModelRepository,
array $supportedFields
) {
$this->productModelRepository = $productModelRepository;
parent::__construct($productModelRepository, $supportedFields);
}
/**
* #param \Pim\Component\Catalog\Model\ProductInterface|\Pim\Component\Catalog\Model\ProductModelInterface $product
* #param string $field
* #param mixed $data
* #param array $options
*/
public function setFieldData($product, $field, $data, array $options = []): void
{
try {
parent::setFieldData($product, $field, $data, $options);
} catch (ImmutablePropertyException $exception) {
if ($exception->getPropertyName() === 'parent') {
// Allow us to change the product parent:
if ($parent = $this->productModelRepository->findOneByIdentifier($data)) {
$familyVariant = $parent->getFamilyVariant();
$product->setParent($parent);
$product->setFamilyVariant($familyVariant);
if (null === $product->getFamily()) {
$product->setFamily($familyVariant->getFamily());
}
}
} else {
throw $exception;
}
}
}
}
This works. Now, upon importing the parent gets saved properly. I'm only wondering if:
a). This implementation is correct.
b). I'm not causing some other major issues by changing the parent.
I also noted the following TODO-statement in the original Akeneo-code above the code that throws the error when attempting to change the parent:
// TODO: This is to be removed in PIM-6350.
Anyone from Akeneo care to shed some light on this?