TYPO3 Extbase individual code on backend-deletion of an object - typo3

I would like to execute some individual code when one of my Extbase domain objects is deleted from the list view in TYPO3 backend.
Thought that it could / would work by overwriting the remove( $o ) method in the according repository like
public function remove( $object ) {
parent::__remove( $object );
do_something_i_want();
}
, but that won't work I guess. Looks like the repository-methods are only called / used by actions of my extension (e.g. if I had some delete-action in a FE- or BE-plugin) but not when the object is just deleted from list view in the backend? I don't use (up to now) any FE/BE-plugin / -actions - only the simple add/edit/delete functions in the backends list view of my storage folder.
Background: I have e.g. two models with some 1:n relation (let's say singer and song), where one object includes some uploaded file (album_cover > pointing to the file's URL in /uploads/myext/ folder); using #cascade works fine for deleting every song belonging to a singer that is deleted, but it won't touch the file uploaded (only) for song.album_cover - leading to quite some waste over time. So I would love to do some sort of onDeletionOfSinger() { deleteAllFilesForHisSongs(); }-thing.
(Same thing would apply on deletion of let's say a single song and it's album_cover-file.)
Sounds quite easy & common, but I just don't get behind it and found nothing useful - would love some hint / pointing to the right direction :-).

List view uses TCEmain hooks during its operations, so you can use one of them to intersect delete action, i.e.: processCmdmap_deleteAction
Register your hooks class in typo3conf/ext/your_ext/ext_tables.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass'][] = 'VENDORNAME\\YourExt\\Hooks\\ProcessCmdmap';
Create a class with valid namespace and path (according to previous step)
file: typo3conf/ext/your_ext/Classes/Hooks/ProcessCmdmap.php
<?php
namespace VENDORNAME\YourExt\Hooks;
class ProcessCmdmap {
/**
* hook that is called when an element shall get deleted
*
* #param string $table the table of the record
* #param integer $id the ID of the record
* #param array $record The accordant database record
* #param boolean $recordWasDeleted can be set so that other hooks or
* #param DataHandler $tcemainObj reference to the main tcemain object
* #return void
*/
function processCmdmap_postProcess($command, $table, $id, $value, $dataHandler) {
if ($command == 'delete' && $table == 'tx_yourext_domain_model_something') {
// Perform something before real delete
// You don't need to delete the record here it will be deleted by CMD after the hook
}
}
}
Don't forget to clear system cache after registering new hook's class

In addition to biesiors answer I want to point out, that there is also a signalSlot for this. So you can rather register on that signal than hooking into tcemain.
in your ext_localconf.php put:
$signalSlotDispatcher =
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\SignalSlot\\Dispatcher');
$signalSlotDispatcher->connect(
'TYPO3\CMS\Extbase\Persistence\Generic\Backend',
'afterRemoveObject',
'Vendor\MxExtension\Slots\MyAfterRemoveObjectSlot',
'myAfterRemoveObjectMethod'
);
So in your Slot you have this PHP file:
namespace Vendor\MxExtension\Slots;
class MyAfterRemoveObjectSlot {
public function myAfterRemoveObjectMethod($object) {
// do something
}
}
Note thet $object will be the $object that was just removed from the DB.
For more information, see https://usetypo3.com/signals-and-hooks-in-typo3.html

Related

How to see AJAX query behind list view in Backpack?

Is it possible in Backpack 4.1 to view the actual query that is run behind a list view, the query that is AJAXed? Would be useful for debugging purposes.
Having a look at the query builder object being used is pretty trivial, you can get it in a controller via $this->crud->query. However, getting the actual query that will be run on your DB with the PDO bindings in place for any where clauses that might be applied etc will take a little bit of setup.
You should be able to do this:
1) Make a file like app/Helpers/Database.php with this content:
if (! function_exists('asSql')) {
/**
* Combines SQL and its bindings
*
* #param \Eloquent | \Illuminate\Database\Eloquent\Builder | \Illuminate\Database\Query\Builder $query
* #return string
*/
function asSql($query)
{
return vsprintf(str_replace('?', '%s', $query->toSql()), collect($query->getBindings())->map(static function ($binding) {
$binding = addslashes($binding);
return is_numeric($binding) ? $binding : "'{$binding}'";
})->toArray());
}
}
2) In app/Providers/AppServiceProvider.php add this to register that new helper method:
/**
* Register services.
*
* #return void
*/
public function register()
{
// require all files in app/Helpers/ so their functions get added globally
foreach (glob(app_path('Helpers') . '/*.php') as $file) {
require_once $file;
}
}
3) With phpstorm (or similar IDE) set a breakpoint in vendor/backpack/crud/src/app/Library/CrudPanel/Traits/Read.php->getEntries()
set your breakpoint on this line $entries = $this->query->get();
If you're in php storm, load a list page and let the breakpoint hit, open the evaluator tool and run asSql($this->query) and you will see the fully bound query that will be run to get your records.
Like this:
This is the query for my user CRUD for which I've added the below in my setup:
$this->crud->addClause('where', 'active', 1);
$this->crud->orderBy('email');
NOTE, this probably wont show queries run to get the relationships for those models if applied, that gets a lot trickier in some cases.
If you are not running php storm with xdebug, or you just dont want to do it there, you could also add do this from your crud controller at the bottom of you setupListOperation method with something like
$sql = asSql($this->crud->query);
dd($sql);

TYPO3 Hook for Page/ Content

I found this Answer here on stackoverflow.
I need a Hook, which is executed when a page and content is created, deleted, moved or updated.
I only found this hook processDatamap_postProcessFieldArray but it will not be executed if the content is created, deleted, moved or updated. It is executed only when a page is created or deleted.
I'm on TYPO3 Version 7.6.9.
Is there a list of all available hooks?
Greetings.
Check out this answer. It has a detailed explanation on how to set up a hook that executes upon record deletion and will certainly help you out.
To sum it up, you need to register your hook in an ext_tables.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['YourHook'][] = 'YourVendor\\YourExt\\Hooks\\YourHook';
And declare the hook itself in:
/ext/your_ext/Classes/Hooks/yourHook.php
Here is a partial list of available hooks from the official Docs.
Edit
Your are looking for the correct Member Function.
To be honest, I am not sure if you need to hook multiple of them or if using processCmdmap_afterFinish will do what you need:
<?php
namespace YourVendor\YourExt\Hooks;
class ProcessCmdmap {
/**
* hook that is called when an element shall get deleted
*
* #param string $table the table of the record
* #param integer $id the ID of the record
* #param array $record The accordant database record
* #param boolean $recordWasDeleted can be set so that other hooks or
* #param DataHandler $tcemainObj reference to the main tcemain object
* #return void
*/
function processCmdmap_postProcess($command, $table, $id, $value, $dataHandler) {
/* Does this trigger at all for the actions you need? */
\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($command);
die();
if ($command == 'delete' ||
$command == 'update' ||
$command == 'move' ||
$table == 'tx_yourext_domain_model_something') {
}
}
}
large portions of this code come from this answer

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 :-)

Delete file when deleting sys_file_reference

I am writing an extension which allows to upload files in the frontend and backend of a TYPO3 instance. The upload works in both views but if the admin wants to delete an upload in the backend in list view, the "physical" file, which is located on the harddisk of the webserver, will not be deleted, only the sys_file_reference record.
Is there a possibility to tell the tca that in case of a deletion of the upload record the associated file should also be deleted? I've also tried to implement a slot with the following code but nothing happens:
ext_localconf.php:
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\SignalSlot\\Dispatcher')->connect(
'TYPO3\CMS\Extbase\Persistence\Generic\Backend',
'afterRemoveObject',
'Kmi\feupload\Slots\MyAfterRemoveObjectSlot',
'myAfterRemoveObjectMethod'
);
Classes/Slots/MyAfterRemoveObjectSlot.php:
namespace Kmi\feupload\Slots;
class MyAfterRemoveObjectSlot {
public function myAfterRemoveObjectMethod($object) {
// do something
\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump($object);
}
}
Has anyone an idea how to solve this? There will be many uploads and if the admin deletes one, the associated file should also be deleted...
Thank you in advance for your help :)
Unfortunately I don't have time to create a complete, tested answer ATM but I'm putting together the steps needed and hope that you can work a solution and complete my answer then.
Every manipulation done through a TCEFORM is saved with the DataHandler (formerly called TCEmain). The DataHandler has numerous hooks. I assume that your model "Upload" has a property file which is of type (or extends) \TYPO3\CMS\Extbase\Domain\Model\FileReference.
File references in TCEFORM are added as IRRE elements. So when you remove the file reference and save the Upload object, the following data is (amogst others) sent to DataHandler:
cmd[sys_file_reference][15011][delete]=1
This means that the file reference with uid 15011 must be deleted. I suggest to implement the processCmdmap_deleteAction hook for this.
So you must also check the datamap to find out if the command was executed through a manipulation of an "Upload" record.
ext_localconf.php:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['your_extension'] = 'My\\Extension\\Hook\\DataHandler';
EXT:your_extension/Classes/Hook/DataHandler.php
This code is untested!
<?php
namespace My\Extension\Hook
class DataHandler {
/**
* #param string $table
* #param int $id
* #param array $recordToDelete
* #param $parentObject \TYPO3\CMS\Core\DataHandling\DataHandler
*/
public function processCmdmap_deleteAction($table, $id, $recordToDelete, $parentObject) {
if (array_key_exists('tx_myext_domain_model_upload', $parentObject->datamap)) {
// Parent record of record to delete is of type "tx_myext_domain_model_upload"
if ($table === 'sys_file_reference' && is_integer($id)) {
// A file reference was requested to delete
// Get an instance of the ResourceFactory
/** #var $resourceFactory \TYPO3\CMS\Core\Resource\ResourceFactory */
$resourceFactory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\ResourceFactory');
// We get the FileReference object for the given id
$fileReferenceObject = $resourceFactory->getFileReferenceObject($id);
// Delete the original file of the file reference
$fileWasDeleted = $fileReferenceObject->getOriginalFile()->delete();
// #TODO throw a warning if $fileWasDeleted is false
}
}
}
}
I commented the code so you know which checks are necessary for what.
Don't forget to clear the system cache after defining the hook in ext_localconf.php.
// delete video or image from sys_file table and sys_file_reference
// table (here videourl - sys_file_reference fieldname)
$fileRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\FileRepository::class);
$fileObjects = $fileRepository->findByRelation('tablename', 'videourl', $id);
foreach ($fileObjects as $fileKey => $fileValue) {
$delete= $fileValue->getOriginalFile()->delete();
}

ManyToOne with FOSUSerBundle ignoring exclusion policy

Building a JSON response for an API type thing, to retrieve a specific set of data that includes a ManyToOne relationship in the entity for my entity that extends FOSUSerBundle's User entity (called Account in my case).
The problem is, the Account entity thats included as a field in the response, is wanted, but I dont want to include all of the password and role type stuff.
I've been browing the internet for a couple hours now, and I've followed many guides on this, and I've cleared my cache every single time, and to no avail; So here's where I ended up:
// app/config/config.yml
jms_serializer:
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/Resources/serializer/FOS"
I've for below I've tried User.Model.yml and Model.User.yml and User.Entity.yml as well in a vain thought that the file name actually matters
// app/Resources/serializer/FOS/Entity.User.yml
FOS\UserBundle\Model\User:
exclusion_policy: ALL
properties:
id:
expose: true
and what I get still looks like this:
{
"status":"ok",
"api_version":"1.0",
"code":200,
"data":{
"video":{
"id":1,
"published":true,
"visibility":true,
"title":"Megaman 2",
"slug":"megaman-2",
"summary":"A rap song about Megaman",
"description":"A rap song\r\nAbout megaman",
"youtube_id":"R6L9bUouDr8",
"date_published":"2014-07-02T14:09:26-0700",
"date_created":"2014-07-02T14:09:26-0700",
"date_updated":"2014-07-02T14:09:26-0700",
"author_id":3,
"author":{
"id":3,
"username":"kharrison",
"username_canonical":"kharrison",
"email":"(sorry private)",
"email_canonical":"(sorry, private)",
"enabled":true,
"salt":"(sorry, private)",
"password":"(sorry, private)",
"last_login":"2014-07-04T15:17:34-0700",
"locked":false,
"expired":false,
"roles":[
"ROLE_SUPER_ADMIN"
],
"credentials_expired":false,
"display_name":"Kyle Harrison",
"slug":"kyle-harrison",
"bio":"Test"
}
}
}
}
The "author" field, is my Account entity thats being run through the JMSSerializer
I want to exclude ALL of that, except the user ID, Display name, and slug.
And finally this is how the API works:
// My/Bundle/Controller/BaseAPIController.php
//......... other code
/**
* #param string $status
* #param integer $code
* #return Response
*/
public function render_api($status, $code)
{
$this->apiResponse->setStatus($status);
$this->apiResponse->setCode($code);
return new Response($this->apiResponse->serialize($this->get('jms_serializer')), $this->apiResponse->getCode(), ["Content-type"=>"application/json"]);
}
//............. other code
and finally, that calls this:
// My/Bundle/Models
class APIResponse {
protected $status;
protected $apiVersion;
protected $code;
protected $data;
public function __construct($apiVersion, $status = "OK", $code = 500)
{
$this->status = $status;
$this->code = $code;
$this->apiVersion = $apiVersion;
$this->data = [];
}
// ... getters and setters
/**
* #return mixed
*/
public function serialize($serializer) {
return $serializer->serialize($this, "json");
}
}
I've for below I've tried User.Model.yml and Model.User.yml and
User.Entity.yml as well in a vain thought that the file name actually
matters.
It does matter, actually. It's a concatenation of the namespace and class name. In this case, you're trying to configure the FOS\UserBundle\Model\User class, so the file name should be Model.User.yml. (FOS\UserBundle\ should be excluded from the file name, since you configured it as namespace_prefix in your config.yml)
Also make sure that your Account class doesn't re-declare (overwrite) the properties, as the serializer config only works if you configure it for the class that actually declares the properties.
Ok So, the actual answer, couldn't have been arrived to via the information I provided. But Nic's Answer did lead me towards the solution. The description of how the the serializer looks at and deciphers the config file lead me to the real problem at hand.
This is what I failed to show:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends BaseUser
{
The problem lays with the Alias I provided the FOS\UserBundle\Model\User namespace. I no longer remember why I wrote that that way. However, the moment I remove the Alias and rewrote the extends to resemble this instead:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends User
{
combined with the new correct filename from Nic's answer, the config based Exclusion policy for JMSSerializerBundle totally kicks in, and every instance of FOSUserBundle's items are now completely hidden, except for the fields I've now explicitly told it to expose.
This is exactly what I wanted :)
Thanks everyone for your help! Cheers
~k
I'm not sure it's the exact way you want it, more a way around:
way around 1: Select only the properties you want (via the entity manager) and then serialize the array obtained.
It's what I do with what I call my API (which is not a class as you but controllers)