Creating unique profile preferences and overriding Account functions

deadbydawn

Member
Hi all,

I am attempting to create an add-on that would allow me to make unique preference options for my users. For example, some of them do not like the Facebook/Twitter integration (mostly the calls to external sites) and so I'd like to give them a method to opt-out.

I began experimenting a little and I am most of the way there, but I have a few questions about saving, and the methodology I am using to approach this (and the sanity therein).

I added a column to the xf_user_option table called "enable_ext_social_includes "which holds a boolean value. I was happy to see that this is automatically populated amongst the $visitor array on every page, so it makes for an easy way to always know. The other benefit to using the existing table and not creating my own is that I don't have to do any JOIN's or extra DB calls to get the variable.

Now that I have this in the database, it was simple enough to create my own template to display the user option on the account preferences template which gives the checkbox option in the same format as used in the rest of the template:


HTML:
    <dl class="ctrlUnit">
        <dt></dt>
        <dd>
            <ul>
                <li><label for="ctrl_enable_ext_social_includes"><input type="checkbox" name="enable_ext_social_includes" value="1" id="ctrl_enable_ext_social_includes" {xen:checked "{$visitor.enable_ext_social_includes}"} /> Allow external includes (javascript) from popular social networks (facebook, twitter, etc)</label></li>
            </ul>
        </dd>
    </dl>



I then created an event listener and a ControllerPublic php page with the following stolen from the core Account.php Controller:


PHP:
<?php

class DBDPreferences_ControllerPublic_DBDPreferences extends XFCP_DBDPreferences_ControllerPublic_DBDPreferences
{

                public function actionPreferencesSave()
                {
                                $this->_assertPostOnly();

                                $settings = $this->_input->filter(array(
                                                //user
                                                'language_id' => XenForo_Input::UINT,
                                                'style_id' => XenForo_Input::UINT,
                                                'visible' => XenForo_Input::UINT,
                                                'timezone' => XenForo_Input::STRING,
                                                //user_option
                                                'content_show_signature' => XenForo_Input::UINT,
                                                'enable_rte' => XenForo_Input::UINT,
                                ));


                                if ($this->_input->filterSingle('default_watch_state', XenForo_Input::UINT))
                                {
                                                $settings['default_watch_state'] = ($this->_input->filterSingle('default_watch_state_email', XenForo_Input::UINT)
                                                                ? 'watch_email'
                                                                : 'watch_no_email'
                                                );
                                }
                                else
                                {
                                                $settings['default_watch_state'] = '';
                                }

                                if (!$this->_saveVisitorSettings($settings, $errors))
                                {
                                                return $this->responseError($errors);
                                }

                                return $this->responseRedirect(
                                                XenForo_ControllerResponse_Redirect::SUCCESS,
                                                XenForo_Link::buildPublicLink('account/preferences')
                                );
                }
}


I was hoping I could override this function and simply add another line into the $settings array as follows to save any changes to that checkbox:

PHP:
                                                .......
                                                'content_show_signature' => XenForo_Input::UINT,
                                                'enable_rte' => XenForo_Input::UINT,
                                                'enable_ext_social_includes' => XenForo_Input::UINT;
                                ));

But doing so elicits the following error when trying to save my user preferences:

The field 'enable_ext_social_includes' was not recognised.

So I assume that there is some form of sanitizing being done in the core where the available fields in the xf_user_option table are hardcoded.. and this is a path I don't want to go down, of course. So my question to you is..

is there a better way? is my logic inherently flawed in thinking it wise to use the existing table rather than create my own? and if so, got any good pointers on how to do JOINs in a clever way since I'm a sql noob? ;)

Thanks all!
 
I was able to create a workaround by doing this:

PHP:
                                $userId = XenForo_Visitor::getUserId();

                                $enable_ext_social_includes = $this->_input->filterSingle('enable_ext_social_includes', XenForo_Input::UINT)
;
                                $db = XenForo_Application::get('db');
                                $db->query("update `xf_user_option` set `enable_ext_social_includes` = $enable_ext_social_includes WHERE `user_id` = $userId ");

But this is not an ideal solution since it means an additional db call. I looked at the original code again and see that it appears to be doing the saving of the settings by calling the protected function saveVisitorSettings. I found that in the Account.php core file and I don't see anything listed there that jumps out as stopping me.. but I do see it making a call to the writer. I'm still reeeeeaaaallly new to all this OOP stuff so now I need to go and read more.. but I feel as if I'm getting closer.
 
The field 'enable_ext_social_includes' was not recognised.
You need to dynamically extend the datawriter that handles the "xf_user_option" table.
Override the _getFields() method and append your custom field information there.
 
You need to dynamically extend the datawriter that handles the "xf_user_option" table.
Override the _getFields() method and append your custom field information there.

Thanks, Shadab.. I have tried googling around a bit for a guide on extending the datawriter, but I'm not having much luck. I see kier's post about extending a controller but I don't seem to be savvy enough to apply that to this. Anyone have any suggestions on where I should turn? I'm mostly learning this through sheer determination as I hardly consider myself a programmer, but I'm doing what I can ;)
 
I found the _getFields() method inside of DataWriter/User.php (below). I believe I understand that I would simply copy all of this into an my addons directory following the same structure as it is in the core, and add an additional line to the array down below that has the similar profile preferences.

Part of what confuses me is that this is a protected function. Again, I'm new to OOP so am I correct to assume that we're allowed to override it because of how the event handler calls my addon?

And can I extend multiple methods within one Listener? I'm extending 'XenForo_ControllerPublic_Account' currently.

Code:
        /**
        * Gets the fields that are defined for the table. See parent for explanation.
        *
        * @return array
        */
        protected function _getFields()
        {
/* trimmed down to just what i need to show */
                        'xf_user_option' => array(
                                'user_id'
                                        => array('type' => self::TYPE_UINT, 'default' => array('xf_user', 'user_id'), 'required' => true),
                                'show_dob_year'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'show_dob_date'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'content_show_signature'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'receive_admin_email'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'email_on_conversation'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'is_discouraged'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 0),
                                'default_watch_state'
                                        => array('type' => self::TYPE_STRING, 'default' => '', 'allowedValues' => array('', 'watch_no_email', 'watch_email')),
                                'alert_optout'
                                        => array('type' => self::TYPE_STRING, 'default' => ''),
                                'enable_rte'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                                'enable_ext_social_includes'
                                        => array('type' => self::TYPE_BOOLEAN, 'default' => 1),
                        ),
....
 
You can extend a datawriter exactly how you have extended a controller.
Then in your extended class, place this method...

PHP:
protected function _getFields()
{
	// Get the original fieldset from the parent class (XenForo_DataWriter_User)
	$fields = parent::_getFields();

	// Add your own field
	$fields['xf_user_option']['enable_ext_social_includes'] = array('type' => self::TYPE_UINT, 'default' => 0);

	return $fields;
}

You don't have to copy the entire parent method in your overriding method.
That's exactly what the call to "parent::" does.

And it's a protected method (not private), so you can still call it from a derived class.
 
I apologize, I feel like you've clearly spelled it out for me, but I'm still missing something.. My add-on name is DBDPreferences, and my directory structure is:

DBDPreferences/Listener/LoadClassController.php
DBDPreferences/Listener/SavePrefs.php
DBDPreferences/ControllerPublic/DBDPreferences.php
DBDPreferences/ControllerPublic/SavePrefs.php

The LoadClassController listener and DBDPreferences.php handles is where I am extending 'XenForo_ControllerPublic_Account' as shown in my first post. SavePrefs.php is where I am trying to extend the XenForo_DataWriter class.

I have created a new Code Event Listener to try to do this:
Listen to Event: load_class_controller
Execute Callback: DBDPreferences_Listener_SavePrefs :: loadClassListener

And my Listener/SavePrefs.php is:


PHP:
<?php

class DBDPreferences_Listener_SavePrefs
{
        public static function loadClassListener($class, &$extend)
        {

                if ($class == 'XenForo_DataWriter')
                {
                        $extend[] = 'DBDPreferences_ControllerPublic_SavePrefs';
                }

        }

}

Finally, my ControllerPublic/SavePrefs.php is:

PHP:
<?php
class DBDPreferences_DataWriter_SavePrefs extends XenForo_DataWriter
{

        protected function _getFields()
        {
            // Get the original fieldset from the parent class (XenForo_DataWriter_User)
            $fields = parent::_getFields();

            // Add your own field
            $fields['xf_user_option']['enable_ext_social_includes'] = array('type' => self::TYPE_UINT, 'default' => 0);

            return $fields;
        }
}

I know that this is being triggered because trying to refresh the Preferences page results in the following error:

Fatal error: Class DBDPreferences_DataWriter_SavePrefs contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (XenForo_DataWriter::_getExistingData, XenForo_DataWriter::_getUpdateCondition) in /var/www/forums/library/DBDPreferences/ControllerPublic/SavePrefs.php on line 3​

I have tried various maneuvers to do what I think the error is saying, as well as switching around more things that I can remember, all to no avail.

Hopefully one of you fine folks sees an obvious error on my part, and hopefully it helps someone else in the future ;)

Thanks!

 
This should do...
PHP:
<?php

class DBDPreferences_Listener_SavePrefs
{
        public static function loadClassListener($class, &$extend)
        {
                // Extend the concrete "User" datawriter.
                // Not the abstract XenForo_DataWriter class.

                if ($class == 'XenForo_DataWriter_User')
                {
                        $extend[] = 'DBDPreferences_DataWriter_SavePrefs';
                }
        }
}

PHP:
<?php

# File: library/DBDPreferences/DataWriter/SavePrefs.php

// Since you are dynamically extending the User datawriter with THIS class,
// you need to extend a pseudo class: XFCP_*, which xenforo evaluates at runtime.

class DBDPreferences_DataWriter_SavePrefs extends XFCP_DBDPreferences_DataWriter_SavePrefs
{
        protected function _getFields()
        {
                // Get the original fieldset from the parent class (XenForo_DataWriter_User)
                $fields = parent::_getFields();

                // Add your own field
                $fields['xf_user_option']['enable_ext_social_includes'] = array('type' => self::TYPE_UINT, 'default' => 0);

                return $fields;
        }
}
 
Thanks for all your help, Shadab! I finally got it working with one key piece of the puzzle missing. load_class_controller won't work because... well, it's a datawriter! So, I had to create an event listener for that, and then extend it since I'm overloading a writer that is already in use by the system (rather than writing something to my own table).

Listen to Event: load_class_datawriter
Execute Callback: DBDPreferences_Listener_LoadClassDataWriter :: loadClassListener

PHP:
<?php
//Filename: DBDPreferences/Listener/LoadClassDataWriter.php

class DBDPreferences_Listener_LoadClassDataWriter
{
        public static function loadClassListener($class, &$extend)
        {
                if ($class == 'XenForo_DataWriter_User')
                {
                        $extend[] = 'DBDPreferences_DataWriter_DBDPreferences';
                }

        }

}

PHP:
<?php
// Filename: DBDPreferences/DataWriter/DBDPreferences.php

class DBDPreferences_DataWriter_DBDPreferences extends XFCP_DBDPreferences_DataWriter_DBDPreferences
{
protected function _getFields()
{
    // Get the original fieldset from the parent class (XenForo_DataWriter_User)
    $fields = parent::_getFields();

    // Add your own field
    $fields['xf_user_option']['enable_ext_social_includes'] = array('type' => self::TYPE_UINT, 'default' => 0);

    return $fields;
}
}
 
Top Bottom