XF 2.0 Are there normalization procedures in XF2?

Jaxel

Well-known member
In XF1, when a user's username was changed, you could normalize this across your addons by hooking into init_dependencies
Code:
public static function listen(XenForo_Dependencies_Abstract $dependencies, array $data)
{
    XenForo_DataWriter_User::$usernameChangeUpdates['permanent']['ewrtorneo_events_username'] = array('EWRtorneo_events', 'username', 'user_id');
    XenForo_DataWriter_User::$usernameChangeUpdates['permanent']['ewrtorneo_results_username'] = array('EWRtorneo_results', 'username', 'user_id');
}

Do such procedures exist in XF2? What about normalizing across user merges?
 
user_content_change_init
user_delete_clean_init
user_merge_combine



PHP:
<?php

namespace Your\Namespace\EventListener;

/**
 * Class UserManagement
 * @package Your\Namespace\EventListener
 */
class UserManagement
{
    /**
     * @param \XF\Service\User\ContentChange $changeService
     * @param array $updates
     */
    public static function userContentChangeInit(\XF\Service\User\ContentChange $changeService, array &$updates)
    {
        $updates['xf_table_name'] = ['user_id', 'emptyable' => false];
        $updates['xf_table_name_2'] = ['user_id', 'username'];
    }

    /**
     * @param \XF\Service\User\DeleteCleanUp $deleteService
     * @param array $deletes
     */
    public static function userDeleteCleanInit(\XF\Service\User\DeleteCleanUp $deleteService, array &$deletes)
    {
        $deletes['xf_table_name'] = 'user_id = ?';

    }

    /**
     * @param \XF\Entity\User $target
     * @param \XF\Entity\User $source
     * @param \XF\Service\User\Merge $mergeService
     */
    public static function userMergeCombine(
        \XF\Entity\User $target, \XF\Entity\User $source, \XF\Service\User\Merge $mergeService
    )
    {
        $target->user_column_name += $source->user_column_name;
    }
}

Notes:
  1. In userContentChangeInit, empytable means whether the table only has a user_id record and not a "backup" username column. If it's not emptyable, the content will be erased (I believe)
  2. In userMergeCombine, please make sure the column names you merge there are also defined in an extension to the User entity structure, otherwise this will fail and (as Mike put it) magical behaviour can occur.

Fillip
 
Uhh... I'm really confused with what you posted...

Code:
public static function userContentChangeInit(\XF\Service\User\ContentChange $changeService, array &$updates)
{
    $updates['ewr_atendo_rsvps'] = ['user_id', 'username'];
}
I assume this will change the username on rows in ewr_atendo_rsvps where the user_id matches?

Code:
public static function userDeleteCleanInit(\XF\Service\User\DeleteCleanUp $deleteService, array &$deletes)
{
    $deletes['ewr_atendo_rsvps'] = 'user_id = ?';
}
This will delete ENTIRE ROWS in ewr_atendo_rsvps where user_id matches the newly deleted user?

Code:
public static function userMergeCombine(\XF\Entity\User $target, \XF\Entity\User $source, \XF\Service\User\Merge $mergeService)
{
    $target->user_column_name += $source->user_column_name;
}
I have absolutely no idea whats going on here... I dont extend the User entity in my addons... so I assume I can just ignore this one. But I want to make it so that if two users are merged, RSVPs that belong to the source user, get moved to the target user. I assume I can do this?
Code:
        $this->app->db->update('ewr_atendo_rsvps',
            ['user_id' => $target['user_id']],
            'user_id = ?', $source['user_id'],
            'IGNORE');
 
Last edited:
That's entirely correct, yeah. You just need to create the 3 event listeners and point them to the file containing those 3 functions and everything you did should be good to go :)

Unrelated, but would you mind making a write-up in the Development Tutorials category in the resource manager here for how to implement the Infinite Scroll you used in XenPorta (which InfScroll library you used, the configuration options, any extra CSS / JS / HTML, etc)?
I could use something like that for one of my projects and I'm sure it would benefit others as well, the animations in particular were pretty cool :)


Fillip
 
I'll look into it...

What does this do?
Code:
$updates['xf_table_name'] = ['user_id', 'emptyable' => false];
 
I'll look into it...

What does this do?
Code:
$updates['xf_table_name'] = ['user_id', 'emptyable' => false];
I've looked into the code for this.

If the user_id is being set to 0 but emptyable is set to false, then this table's contents is not touched. The user ID is left as-is.
If the user_id is being deleted, then any entries from that table matching that user_id is removed regardless of whether emptyable is false or not.

Basically, you want to set emptyable to false if you want to keep the original user_id because setting it to 0 would break stuff.


Fillip
 
That makes me even more confused...

What purpose does that serve when there already is an event for deleting users at user_delete_clean_init?

How would I combine both functions?
Code:
$updates['ewr_atendo_rsvps'] = ['user_id', 'username', 'emptyable' => false];

userMergeCombine and userDeleteCleanInit make sense now... userContentChangeInit makes no sense to me.

All I want to do is match username changes across my addon.
 
Content change is generic. It's used for renames when there's a user name field, user merges (changing between 2 user IDs) and user deletes (changing the user ID to 0).

The other events are more type specific actions, such as actually removing rows or doing some more arbitrary actions.
 
Content change is generic. It's used for renames when there's a user name field, user merges (changing between 2 user IDs) and user deletes (changing the user ID to 0).

The other events are more type specific actions, such as actually removing rows or doing some more arbitrary actions.
So what would I put in user_content_change_init to sync username changes? User merges? and user deletes?

And does that mean I DON'T need user_delete_clean_init and user_merge_combine?
 
Last edited:
Looking into ContentChange... it looks like

$updates['unique_id'][0] = the user ID column to match
$updates['unique_id'][1] = the username to change if needed

These two lines appear to handle user merges and username changes by themselves.

$updates['unique_id']['emptyable'] = I'm not really sure... it's hard to follow whats going on here. It looks like if the new user ID would be "0" (I assume on a user DELETE? is this correct?), it ignores the content... but then I'm confused why you would need to define this field as "false", since it should be empty in the first place by the nature of it not existing.

How would I get it to DELETE the row, on a user delete?
 
Last edited:
When a user is deleted, there are roughly 3 options of dealing with the data associated with that user:
  1. Keep it, but change the user_id (to 0)
  2. Keep it, leaving the user_id intact
  3. Delete the row
The content change system handles the first two; the deletion service handles the last. The content change system never does a delete.

Emptyable controls whether you want to do the user_id update when changing the user_id to 0. If false, it's skipped. You'd to this in cases where you don't want to change the ID or you're going to be deleting the row via the deletion service and thus the update would either be superfluous (or cause issues). Emptyable defaults to true.
 
So then this would be good?
Code:
class UserManagement
{
    public static function userContentChangeInit(\XF\Service\User\ContentChange $changeService, array &$updates)
    {
        $updates['ewr_atendo_events'] = ['user_id', 'username'];
        $updates['ewr_atendo_rsvps'] = ['user_id'];
    }
    
    public static function userDeleteCleanInit(\XF\Service\User\DeleteCleanUp $deleteService, array &$deletes)
    {
        $deletes['ewr_atendo_rsvps'] = 'user_id = ?';
    }
}

This would change/merge users on those two tables: ewr_atendo_events and ewr_atendo_rsvps.

It would also leave the the initial user IDs in the tables so that user_delete_clean_init can delete the row later. Correct?

I have tested this however, and it doesn't seem to be the case. "Emptyable" does not seem to default to true.
Unless I specifically define 'emptyable' => true, the user_id gets changed to 0.
 
So the emptyable condition was bugged. For the most part, it generally just got ignored. In the vast majority of cases, this just runs superfluous queries as emptyable is generally false when you're deleting the associated data with a user delete anyway. I've fixed this for 2.0.2 so if emptyable is false, the query won't run on in the content changer on user delete.

It would also leave the the initial user IDs in the tables so that user_delete_clean_init can delete the row later. Correct?
I had to double check the code to confirm this, but the deletion happens first anyway. So given your example, on user deletion, you'll leave records in the events table but remove them from the RSVPs table (which logically seems to make sense). On a user merge, you'll update the user IDs in both tables. And on a user rename, you'll update the cached username in the events table.
 
So the emptyable condition was bugged. For the most part, it generally just got ignored. In the vast majority of cases, this just runs superfluous queries as emptyable is generally false when you're deleting the associated data with a user delete anyway. I've fixed this for 2.0.2 so if emptyable is false, the query won't run on in the content changer on user delete.


I had to double check the code to confirm this, but the deletion happens first anyway. So given your example, on user deletion, you'll leave records in the events table but remove them from the RSVPs table (which logically seems to make sense). On a user merge, you'll update the user IDs in both tables. And on a user rename, you'll update the cached username in the events table.
So then the emptyable parameter is irrelevant to me? Great. Thanks!
 
Once fixed, you can mark the RSVPs change as not emptyable (false), simply because it will never do anything since the content has already been deleted.

Provided your table has an index on user_id, this won't be a particularly expensive query anyway. (If it doesn't, it could be slow so skipping the query has more value.)
 
Okay... question... lets say I have a table, where `user_id` is labelled as a unique key.

Then I have the following trigger:
Code:
$updates['my_table'] = ['user_id'];

Now, lets say as a result of the normalization procedure, there would be a violation of the unique constraint. What happens in this case?
 
So now that things have changed in 2.0.0.4... are normalization procedures the same?

Code:
    public static function userContentChangeInit(\XF\Service\User\ContentChange $changeService, array &$updates)
    {
        $updates['ewr_medio_comments'] = ['user_id', 'username'];
        $updates['ewr_medio_media'] = ['user_id', 'username'];
        $updates['ewr_medio_playlists'] = ['user_id'];
        $updates['ewr_medio_userlinks'] = ['user_id'];
    }
The above code should update the first two tables on username changes, and update all 4 tables on user merges... correct?


Code:
    public static function userDeleteCleanInit(\XF\Service\User\DeleteCleanUp $deleteService, array &$deletes)
    {
        $deletes['ewr_medio_playlists'] = 'user_id = ?';
        $deletes['ewr_medio_userlinks'] = 'user_id = ?';
    }
The above could should delete rows rows from the tables on user deletes... correct?


Does emptyable do anything yet? And if so, what does it do?
 
Top Bottom