Extending DataWriter/_preSave but it executes elsewhere too

Marcel

Active member
I've written an addon to add the ability to archive threads.
It adds two colunns/options to the xf_forum table for the Forum Editor so we can set each one to archive to on a per-forum basis.

Here is my DataWriter Code

PHP:
<?php
class MaB_Arch_DataWriter_Forum extends XFCP_MaB_Arch_DataWriter_Forum
{
    protected function _getFields()
    {
        $fields = parent::_getFields();
        $fields['xf_forum']['mab_arch_archive_enabled'] = array(
            'type' => self::TYPE_UINT, 'default' => 0
        );
        $fields['xf_forum']['mab_arch_archive_forumid'] = array(
            'type' => self::TYPE_UINT, 'default' => 0
        );
        return $fields;
    }

    protected function _preSave()
    {
        $parent = parent::_preSave();
        // only run this conditional if we are editing a forum
        if ($GLOBALS['fc']->route()->getControllerName() === 'XenForo_ControllerAdmin_Forum')
            {
            // is the 'enabled' checkbox ticked?
            if(isset($_POST['mab_arch_archive_enabled']))
            {
                // if so store the entered archive forum id into a variable and pass it
                // to the datawriter and set the enabled flag to 1
                $mab_arch_archive_forumid = $_POST['mab_arch_archive_forumid'];
                $this->set('mab_arch_archive_enabled', 1);
                $this->set('mab_arch_archive_forumid', $mab_arch_archive_forumid);
            }
            else
            {
                // if not then ignore what was typed in and just set the archive forum id to 0
                $this->set('mab_arch_archive_enabled', 0);
                $this->set('mab_arch_archive_forumid', NULL);
            }}
        return $parent;
     
    }
}

The problem is the conditional. The code works fine without it, it presents the forum options which I set OK.
As soon as I then archive a thread...the forum options are removed.
I'm assuming because when it's rebuilding the discussion after the move, it's also using the Forum DataWriter, and running through the _PreSave code again...and because there's no forum options in the $POST, it's assuming I've set them as Disabled / 0.

So I put in the conditional, using the discussion from digitalpoint and Mike/Kier here

This works, but the conditional throws an error with TapaTalk, and tbh I'd rather work around it at my end.
 
Why are you extending the DataWriter instead of the Controller? This doesn't belong in the DataWriter.
 
Damn, I had a feeling it should have been the other way round.
I didn't know how to add per-forum options, so with a mix of searching, googling, and trying to understand how other addons did it, I did it this way. I came across both ways while looking it up.

I'm learning all this as I'm going along. Adding new functionality is fine, I'm just not sure on how to extend current functionality (IE adding two more fields to be dealt with via ControllerAdmin_Forum).
 
Forgot to say, I did think of adding a new listener, triggering on ControllerAdmin_Forum, and in that setting a variable.
Then using a simple variable check in the DataWriter.

Although that seemed a wasteful way of doing it.
 
Extend ControllAdmin_Forum::actionSave() so it is saved ($dw->set(things)), use template modification system to add the option to the forum edit template.
 
Thanks for the reply :)

Right, I've been squirrelling away all afternoon and I just couldn't figure out how to get it from the preSave in the DW to the actionSave of of the Forum Admin Controller.
The best I could do was similar to this guy
http://xenforo.com/community/threads/datawriter-not-working-with-my-action-it-save-blank.63154/

However, I *was* getting errors. The rest of the fields (Forum Name etc) were throwing up unnecessary errors.
Then I saw Chris D's reply about having to put the code in the DataWriter/_preSave instead, so that'll do for me lol

I've moved the conditional into the controlleradmin, then populated GLOBALS with the info, so I can pass those back to the datawriter, which then does it's original job.


This is my ControllerAdmin_Forum
PHP:
class MaB_Arch_ControllerAdmin_Forum extends XFCP_MaB_Arch_ControllerAdmin_Forum
{
    public function actionSave()
    {
        // is the 'enabled' checkbox ticked?
        if(isset($_POST['mab_arch_archive_enabled']))
        {
            // if so store the entered archive forum id into a variable and pass it
            // to the datawriter and set the enabled flag to 1
            $mab_arch_archive_forumid = $_POST['mab_arch_archive_forumid'];
            $GLOBALS['mab_arch_archive_enabled'] = 1;
            $GLOBALS['mab_arch_archive_forumid'] = $mab_arch_archive_forumid;
        }
        else
        {
            // if not then ignore what was typed in and just set the archive forum id to 0
            $GLOBALS['mab_arch_archive_enabled'] = 0;
            $GLOBALS['mab_arch_archive_forumid'] = NULL;
        }
        return parent::actionSave();
    }
}

and my DataWriter_Forum
PHP:
    protected function _getFields()
    {
        $fields = parent::_getFields();
        $fields['xf_forum']['mab_arch_archive_enabled'] = array(
            'type' => self::TYPE_UINT, 'default' => 0
        );
        $fields['xf_forum']['mab_arch_archive_forumid'] = array(
            'type' => self::TYPE_UINT, 'default' => 0
        );
        return $fields;
    }

    protected function _preSave()
    {
        $parent = parent::_preSave();
        $this->set('mab_arch_archive_enabled', $GLOBALS['mab_arch_archive_enabled']);
        $this->set('mab_arch_archive_forumid', $GLOBALS['mab_arch_archive_forumid']);
        return $parent;
    }
 
I need to pass the two variables from the controller to the datawriter. Is there a better way of doing it?
 
I'm trying! :p
So you're saying I can just pull the input direct from within the DataWriter?
That's fine....but then we're back to the problem I had at first....I need this to only run on ControllerAdmin_Forum, or else other things fail.
 
@tyteen4a03, there is actually a long standing issue involved in extending controller actions that save data using the DataWriter.

It's not possible to set data after preSave has been called, (so you can't just $parent = parent::actionSave(); /* blah */ return $parent;) and if you call the DataWriter, set your data, you can't save because you need the parent to do that, but then return parent::actionSave() will create a new DataWriter instance overwriting your own.

So what @Marcel is doing is actually correct.

It's discussed in great detail here:

http://xenforo.com/community/threads/more-modular-code.29444/
 
And Mike pretty much confirms that, as it stands right now, the "hacky" workarounds are what is needed:

You will need to find a workaround. That's really all I could say. There may be places that change over time, but there are so many potential locations so it's not like I could say "yes, everything will change" or anything like that.

Hacks they may be, but there are possible workarounds and if you're trying to override bits of behavior within the existing system, that's roughly what's going to be required if the code structure doesn't fit your needs.
 
Thanks Chris, I thought I was going insane for a moment then.
I've now modified the code, and got it all working.
Using this in the Controller
PHP:
    public function actionSave()
    {
        $GLOBALS['mab_arch_controlleradmin_forum'] = $this;
        return parent::actionSave();
    }

and this in the DataWriter
PHP:
    protected function _preSave()
    {
        $parent = parent::_preSave();
        if (isset($GLOBALS['mab_arch_controlleradmin_forum']))
        {
            $this->set('mab_arch_archive_enabled', $GLOBALS['mab_arch_controlleradmin_forum']->getInput()->filterSingle('mab_arch_archive_enabled', XenForo_Input::UINT));
            $this->set('mab_arch_archive_forumid', $GLOBALS['mab_arch_controlleradmin_forum']->getInput()->filterSingle('mab_arch_archive_forumid', XenForo_Input::UINT));
            }
        return $parent;
    }
 
I'll dig out my most recent, preferred workaround.

You can actually see this in my Hide Poll Results add-on:

PHP:
<?php

class HidePollResults_ControllerPublic_Forum extends XFCP_HidePollResults_ControllerPublic_Forum
{
    /**
    * Inserts a new thread into this forum.
    *
    * @return XenForo_ControllerResponse_Abstract
    */
    public function actionAddThread()
    {
        $session = false;
        if (XenForo_Application::isRegistered('session'))
        {
            /** @var $session XenForo_Session */
            $session = XenForo_Application::get('session');
        }

        $hidePollResultsFormShown = $this->_input->filterSingle('hide_poll_results_form', XenForo_Input::UINT);
        if (!$hidePollResultsFormShown || !$session)
        {
            if ($session)
            {
                $session->remove('hidePollResults');
            }

            return parent::actionAddThread();
        }

        $inputData = $this->_input->filter(array(
            'hide_results' => XenForo_Input::BOOLEAN,
            'until_close' => XenForo_Input::BOOLEAN
        ));

        $session->set('hidePollResults', $inputData);
           
        return parent::actionAddThread();
    }
}
So, before the parent is called, get the input data and save it to the user's session.

PHP:
<?php

class HidePollResults_DataWriter_Poll extends XFCP_HidePollResults_DataWriter_Poll
{
    /**
    * Gets the fields that are defined for the table. See parent for explanation.
    *
    * @return array
    */
    protected function _getFields()
    {
        $parent = parent::_getFields();
       
        $parent['xf_poll']['hide_results'] = array('type' => self::TYPE_BOOLEAN, 'default' => 0);
        $parent['xf_poll']['until_close'] =    array('type' => self::TYPE_BOOLEAN,    'default' => 0);
       
        return $parent;
    }
   
    /**
    * Pre-save handling.
    */
    protected function _preSave()
    {
        $session = false;
        if (XenForo_Application::isRegistered('session'))
        {
            /** @var $session XenForo_Session */
            $session = XenForo_Application::get('session');
        }

        if ($session && $session->get('hidePollResults'))
        {
            $this->bulkSet($session->get('hidePollResults'));

            $session->remove('hidePollResults');
        }

        return parent::_preSave();
    }
}
Then, in preSave in the DataWriter, we can grab that data from the session, set it in the DataWriter and then remove the data from the session.
 
@tyteen4a03, there is actually a long standing issue involved in extending controller actions that save data using the DataWriter.

It's not possible to set data after preSave has been called, (so you can't just $parent = parent::actionSave(); /* blah */ return $parent;) and if you call the DataWriter, set your data, you can't save because you need the parent to do that, but then return parent::actionSave() will create a new DataWriter instance overwriting your own.

So what @Marcel is doing is actually correct.

It's discussed in great detail here:

http://xenforo.com/community/threads/more-modular-code.29444/
TIL.

To-fix in XF2 it seems.
 
It depends what it's for I guess. I tend to only use the session now because I know it works and for most tasks I can just copy and paste my existing code.

I started using the session where actually using the session made more sense.

Specifically I had something that was run by users in the front end. I guess there was potential here for there to be a race condition where one user's data could overwrite another. To mitigate that I guess you could store it by user ID in the registry but then by the time you've done that you might as well just be using storage designed to be individual to each user. Also as the session data is temporary in nature there wasn't any risk of old or orphaned data being left in the registry in case of some error.

Overall it works and works well so I'm going to stick to it.

Roll on XF 2.0 where hopefully this is going to be fixed :)
 
Is it bad that I got the updated thread notification, read through the thread and have very little recollection of it? I've even looked through the code I've provided and I'm looking at my screen with a blank look :D

Aaaah, gotta love those caffeine and thumping music-fuelled nights of development :D
 
Top Bottom