2

I need a custom profile plugin, so I copied the default user profile, renamed some folders/files, and installed it. It installs as if fine and I can activate it, but the fields don't actually display in the registration form. I activate the default user profile, and it does display the fields. I feel like that means it's not pulling the customprofile.xml file, but I can't tell why not.
File Structure:
file structure

customprofile.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" group="user" method="upgrade">
    <name>PLG_CUSTOM_USER_PROFILE</name>
    <author>QTown</author>
    <creationDate>2022-12</creationDate>
    <copyright></copyright>
    <license>GNU General Public License version 2 or later; see LICENSE.txt</license>
    <authorEmail>[email protected]</authorEmail>
    <authorUrl>www.mysite.org</authorUrl>
    <version>1.0.0</version>
    <description>PLG_CUSTOM_USER_PROFILE_XML_DESCRIPTION</description>
    <namespace path="src">Qtown\Plugin\User\Customprofile</namespace>
    <files>
        <filename plugin="customprofile">customprofile.php</filename>
        <folder>forms</folder>
        <folder>src</folder>
    </files>
    <languages>
        <language tag="en-GB">language/en-GB/plg_user_customprofile.ini</language>
        <language tag="en-GB">language/en-GB/plg_user_customprofile.sys.ini</language>
    </languages>
    <config>
        <fields name="params">
            <fieldset name="basic" addfieldprefix="Joomla\Component\Content\Administrator\Field">
                <field
                    name="register-require-user"
                    type="spacer"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_NAME_REGISTER_REQUIRE_USER"
                    class="text"
                />

                <field
                    name="register-require_address1"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ADDRESS1_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_address2"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ADDRESS2_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_city"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_CITY_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_region"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_REGION_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_country"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_COUNTRY_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_postal_code"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_POSTAL_CODE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_phone"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_PHONE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_website"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_WEB_SITE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_favoritebook"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_FAVORITE_BOOK_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_aboutme"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ABOUT_ME_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register-require_tos"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_TOS_LABEL"
                    default="0"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="register_tos_article"
                    type="modal_article"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_TOS_ARTICLE_LABEL"
                    select="true"
                    new="true"
                    edit="true"
                    clear="true"
                    filter="integer"
                />

                <field
                    name="register-require_dob"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_DOB_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="spacer1"
                    type="spacer"
                    hr="true"
                />

                <field
                    name="profile-require-user"
                    type="spacer"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_NAME_PROFILE_REQUIRE_USER"
                    class="text"
                />

                <field
                    name="profile-require_address1"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ADDRESS1_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_address2"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ADDRESS2_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_city"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_CITY_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_region"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_REGION_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_country"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_COUNTRY_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_postal_code"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_POSTAL_CODE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_phone"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_PHONE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_website"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_WEB_SITE_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_favoritebook"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_FAVORITE_BOOK_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_aboutme"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_ABOUT_ME_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>

                <field
                    name="profile-require_dob"
                    type="list"
                    label="PLG_CUSTOM_USER_PROFILE_FIELD_DOB_LABEL"
                    default="1"
                    filter="integer"
                    validate="options"
                    >
                    <option value="2">JOPTION_REQUIRED</option>
                    <option value="1">JOPTION_OPTIONAL</option>
                    <option value="0">JDISABLED</option>
                </field>
            </fieldset>
        </fields>
    </config>
</extension>



customprofile.php

<?php

/**
 * @package     Joomla.Plugin
 * @subpackage  User.profile
 *
 * @copyright   (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt

 * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
 */

use Joomla\CMS\Date\Date;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\String\PunycodeHelper;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * An example custom profile plugin.
 *
 * @since  1.6
 */
class PlgUserCustomProfile extends CMSPlugin
{
    /**
     * @var    \Joomla\CMS\Application\CMSApplication
     *
     * @since  4.0.0
     */
    protected $app;

    /**
     * @var    \Joomla\Database\DatabaseDriver
     *
     * @since  4.0.0
     */
    protected $db;

    /**
     * Load the language file on instantiation.
     *
     * @var    boolean
     *
     * @since  3.1
     */
    protected $autoloadLanguage = true;

    /**
     * Date of birth.
     *
     * @var    string
     *
     * @since  3.1
     */
    private $date = '';

    /**
     * Runs on content preparation
     *
     * @param   string  $context  The context for the data
     * @param   object  $data     An object containing the data for the form.
     *
     * @return  boolean
     *
     * @since   1.6
     */
    public function onContentPrepareData($context, $data)
    {
        // Check we are manipulating a valid form.
        if (!in_array($context, ['com_users.profile', 'com_users.user', 'com_users.registration'])) {
            return true;
        }

        if (is_object($data)) {
            $userId = $data->id ?? 0;

            if (!isset($data->profile) && $userId > 0) {
                // Load the profile data from the database.
                $db    = $this->db;
                $query = $db->getQuery(true)
                    ->select(
                        [
                            $db->quoteName('profile_key'),
                            $db->quoteName('profile_value'),
                        ]
                    )
                    ->from($db->quoteName('#__user_profiles'))
                    ->where($db->quoteName('user_id') . ' = :userid')
                    ->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('profile.%'))
                    ->order($db->quoteName('ordering'))
                    ->bind(':userid', $userId, ParameterType::INTEGER);

                $db->setQuery($query);
                $results = $db->loadRowList();

                // Merge the profile data.
                $data->profile = [];

                foreach ($results as $v) {
                    $k = str_replace('profile.', '', $v[0]);
                    $data->profile[$k] = json_decode($v[1], true);

                    if ($data->profile[$k] === null) {
                        $data->profile[$k] = $v[1];
                    }
                }
            }

            if (!HTMLHelper::isRegistered('users.url')) {
                HTMLHelper::register('users.url', [__CLASS__, 'url']);
            }

            if (!HTMLHelper::isRegistered('users.calendar')) {
                HTMLHelper::register('users.calendar', [__CLASS__, 'calendar']);
            }

            if (!HTMLHelper::isRegistered('users.tos')) {
                HTMLHelper::register('users.tos', [__CLASS__, 'tos']);
            }

            if (!HTMLHelper::isRegistered('users.dob')) {
                HTMLHelper::register('users.dob', [__CLASS__, 'dob']);
            }
        }

        return true;
    }

    /**
     * Returns an anchor tag generated from a given value
     *
     * @param   string  $value  URL to use
     *
     * @return  mixed|string
     */
    public static function url($value)
    {
        if (empty($value)) {
            return HTMLHelper::_('users.value', $value);
        } else {
            // Convert website URL to utf8 for display
            $value = PunycodeHelper::urlToUTF8(htmlspecialchars($value));

            if (strpos($value, 'http') === 0) {
                return '<a href="' . $value . '">' . $value . '</a>';
            } else {
                return '<a href="http://' . $value . '">' . $value . '</a>';
            }
        }
    }

    /**
     * Returns html markup showing a date picker
     *
     * @param   string  $value  valid date string
     *
     * @return  mixed
     */
    public static function calendar($value)
    {
        if (empty($value)) {
            return HTMLHelper::_('users.value', $value);
        } else {
            return HTMLHelper::_('date', $value, null, null);
        }
    }

    /**
     * Returns the date of birth formatted and calculated using server timezone.
     *
     * @param   string  $value  valid date string
     *
     * @return  mixed
     */
    public static function dob($value)
    {
        if (!$value) {
            return '';
        }

        return HTMLHelper::_('date', $value, Text::_('DATE_FORMAT_LC1'), false);
    }

    /**
     * Return the translated strings yes or no depending on the value
     *
     * @param   boolean  $value  input value
     *
     * @return  string
     */
    public static function tos($value)
    {
        if ($value) {
            return Text::_('JYES');
        } else {
            return Text::_('JNO');
        }
    }

    /**
     * Adds additional fields to the user editing form
     *
     * @param   Form   $form  The form to be altered.
     * @param   mixed  $data  The associated data for the form.
     *
     * @return  boolean
     *
     * @since   1.6
     */
    public function onContentPrepareForm(Form $form, $data)
    {
        // Check we are manipulating a valid form.
        $name = $form->getName();

        if (!in_array($name, ['com_users.user', 'com_users.profile', 'com_users.registration'])) {
            return true;
        }

        // Add the registration fields to the form.
        FormHelper::addFieldPrefix('Joomla\\Plugin\\User\\Profile\\Field');
        FormHelper::addFormPath(__DIR__ . '/forms');
        $form->loadFile('profile');

        $fields = [
            'address1',
            'address2',
            'city',
            'region',
            'country',
            'postal_code',
            'phone',
            'website',
            'favoritebook',
            'aboutme',
            'dob',
            'tos',
        ];

        $tosArticle = $this->params->get('register_tos_article');
        $tosEnabled = $this->params->get('register-require_tos', 0);

        // We need to be in the registration form and field needs to be enabled
        if ($name !== 'com_users.registration' || !$tosEnabled) {
            // We only want the TOS in the registration form
            $form->removeField('tos', 'profile');
        } else {
            // Push the TOS article ID into the TOS field.
            $form->setFieldAttribute('tos', 'article', $tosArticle, 'profile');
        }

        foreach ($fields as $field) {
            // Case using the users manager in admin
            if ($name === 'com_users.user') {
                // Toggle whether the field is required.
                if ($this->params->get('profile-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('profile-require_' . $field) == 2) ? 'required' : '', 'profile');
                } elseif (
                    // Remove the field if it is disabled in registration and profile
                    $this->params->get('register-require_' . $field, 1) == 0
                    && $this->params->get('profile-require_' . $field, 1) == 0
                ) {
                    $form->removeField($field, 'profile');
                }
            } elseif ($name === 'com_users.registration') {
                // Case registration
                // Toggle whether the field is required.
                if ($this->params->get('register-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('register-require_' . $field) == 2) ? 'required' : '', 'profile');
                } else {
                    $form->removeField($field, 'profile');
                }
            } elseif ($name === 'com_users.profile') {
                // Case profile in site or admin
                // Toggle whether the field is required.
                if ($this->params->get('profile-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('profile-require_' . $field) == 2) ? 'required' : '', 'profile');
                } else {
                    $form->removeField($field, 'profile');
                }
            }
        }

        // Drop the profile form entirely if there aren't any fields to display.
        $remainingfields = $form->getGroup('profile');

        if (!count($remainingfields)) {
            $form->removeGroup('profile');
        }

        return true;
    }

    /**
     * Method is called before user data is stored in the database
     *
     * @param   array    $user   Holds the old user data.
     * @param   boolean  $isnew  True if a new user is stored.
     * @param   array    $data   Holds the new user data.
     *
     * @return  boolean
     *
     * @since   3.1
     * @throws  InvalidArgumentException on invalid date.
     */
    public function onUserBeforeSave($user, $isnew, $data)
    {
        // Check that the date is valid.
        if (!empty($data['profile']['dob'])) {
            try {
                $date = new Date($data['profile']['dob']);
                $this->date = $date->format('Y-m-d H:i:s');
            } catch (Exception $e) {
                // Throw an exception if date is not valid.
                throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_ERROR_INVALID_DOB'));
            }

            if (Date::getInstance('now') < $date) {
                // Throw an exception if dob is greater than now.
                throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_ERROR_INVALID_DOB_FUTURE_DATE'));
            }
        }

        // Check that the tos is checked if required ie only in registration from frontend.
        $task       = $this->app->input->getCmd('task');
        $option     = $this->app->input->getCmd('option');
        $tosEnabled = ($this->params->get('register-require_tos', 0) == 2);

        // Check that the tos is checked.
        if ($task === 'register' && $tosEnabled && $option === 'com_users' && !$data['profile']['tos']) {
            throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_FIELD_TOS_DESC_SITE'));
        }

        return true;
    }

    /**
     * Saves user profile data
     *
     * @param   array    $data    entered user data
     * @param   boolean  $isNew   true if this is a new user
     * @param   boolean  $result  true if saving the user worked
     * @param   string   $error   error message
     *
     * @return  void
     */
    public function onUserAfterSave($data, $isNew, $result, $error): void
    {
        $userId = ArrayHelper::getValue($data, 'id', 0, 'int');

        if ($userId && $result && isset($data['profile']) && count($data['profile'])) {
            $db = $this->db;

            // Sanitize the date
            if (!empty($data['profile']['dob'])) {
                $data['profile']['dob'] = $this->date;
            }

            $keys = array_keys($data['profile']);

            foreach ($keys as &$key) {
                $key = 'profile.' . $key;
            }

            $query = $db->getQuery(true)
                ->delete($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->whereIn($db->quoteName('profile_key'), $keys, ParameterType::STRING)
                ->bind(':userid', $userId, ParameterType::INTEGER);
            $db->setQuery($query);
            $db->execute();

            $query->clear()
                ->select($db->quoteName('ordering'))
                ->from($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->bind(':userid', $userId, ParameterType::INTEGER);
            $db->setQuery($query);
            $usedOrdering = $db->loadColumn();

            $order = 1;
            $query->clear()
                ->insert($db->quoteName('#__user_profiles'));

            foreach ($data['profile'] as $k => $v) {
                while (in_array($order, $usedOrdering)) {
                    $order++;
                }

                $query->values(
                    implode(
                        ',',
                        $query->bindArray(
                            [
                                $userId,
                                'profile.' . $k,
                                json_encode($v),
                                $order++,
                            ],
                            [
                                ParameterType::INTEGER,
                                ParameterType::STRING,
                                ParameterType::STRING,
                                ParameterType::INTEGER,
                            ]
                        )
                    )
                );
            }

            $db->setQuery($query);
            $db->execute();
        }
    }

    /**
     * Remove all user profile information for the given user ID
     *
     * Method is called after user data is deleted from the database
     *
     * @param   array    $user     Holds the user data
     * @param   boolean  $success  True if user was successfully stored in the database
     * @param   string   $msg      Message
     *
     * @return  void
     */
    public function onUserAfterDelete($user, $success, $msg): void
    {
        if (!$success) {
            return;
        }

        $userId = ArrayHelper::getValue($user, 'id', 0, 'int');

        if ($userId) {
            $db = $this->db;
            $query = $db->getQuery(true)
                ->delete($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('profile.%'))
                ->bind(':userid', $userId, ParameterType::INTEGER);

            $db->setQuery($query);
            $db->execute();
        }
    }
}
2
  • Looking at the custom fields you have set are just a repeat of the standard profile fields? So I guess I have to ask why? All my custom profile plugins I've created have been by adding an extension to "profile" such as PlgUserProfilexxxx and then I replace all reference to profile to be "profilexxxx". This then provides extra fields in the #__user_profiles table. Commented Dec 24, 2022 at 1:10
  • The idea is to add new fields, and this is just the start to make sure I understand how the plugin should work.
    – James
    Commented Dec 24, 2022 at 5:14

1 Answer 1

3

The profile key in the database should be the same as your plugin name, ie PlgUserCustomProfile means that your key field should be like

foreach ($data['customprofile'] as $k => $v)

So perhaps try this:

    <?php

/**
 * @package     Joomla.Plugin
 * @subpackage  User.customprofile
 *
 * @copyright   (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt

 * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
 */

use Joomla\CMS\Date\Date;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\String\PunycodeHelper;
use Joomla\Database\ParameterType;
use Joomla\Utilities\ArrayHelper;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * An example custom customprofile plugin.
 *
 * @since  1.6
 */
class PlgUserCustomProfile extends CMSPlugin
{
    /**
     * @var    \Joomla\CMS\Application\CMSApplication
     *
     * @since  4.0.0
     */
    protected $app;

    /**
     * @var    \Joomla\Database\DatabaseDriver
     *
     * @since  4.0.0
     */
    protected $db;

    /**
     * Load the language file on instantiation.
     *
     * @var    boolean
     *
     * @since  3.1
     */
    protected $autoloadLanguage = true;

    /**
     * Date of birth.
     *
     * @var    string
     *
     * @since  3.1
     */
    private $date = '';

    /**
     * Runs on content preparation
     *
     * @param   string  $context  The context for the data
     * @param   object  $data     An object containing the data for the form.
     *
     * @return  boolean
     *
     * @since   1.6
     */
    public function onContentPrepareData($context, $data)
    {
        // Check we are manipulating a valid form.
        if (!in_array($context, ['com_users.profile', 'com_users.user', 'com_users.registration'])) {
            return true;
        }

        if (is_object($data)) {
            $userId = $data->id ?? 0;

            if (!isset($data->customprofile) && $userId > 0) {
                // Load the profile data from the database.
                $db    = $this->db;
                $query = $db->getQuery(true)
                    ->select(
                        [
                            $db->quoteName('profile_key'),
                            $db->quoteName('profile_value'),
                        ]
                    )
                    ->from($db->quoteName('#__user_profiles'))
                    ->where($db->quoteName('user_id') . ' = :userid')
                    ->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('customprofile.%'))
                    ->order($db->quoteName('ordering'))
                    ->bind(':userid', $userId, ParameterType::INTEGER);

                $db->setQuery($query);
                $results = $db->loadRowList();

                // Merge the profile data.
                $data->customprofile = [];

                foreach ($results as $v) {
                    $k = str_replace('customprofile.', '', $v[0]);
                    $data->customprofile[$k] = json_decode($v[1], true);

                    if ($data->customprofile[$k] === null) {
                        $data->customprofile[$k] = $v[1];
                    }
                }
            }

            if (!HTMLHelper::isRegistered('users.url')) {
                HTMLHelper::register('users.url', [__CLASS__, 'url']);
            }

            if (!HTMLHelper::isRegistered('users.calendar')) {
                HTMLHelper::register('users.calendar', [__CLASS__, 'calendar']);
            }

            if (!HTMLHelper::isRegistered('users.tos')) {
                HTMLHelper::register('users.tos', [__CLASS__, 'tos']);
            }

            if (!HTMLHelper::isRegistered('users.dob')) {
                HTMLHelper::register('users.dob', [__CLASS__, 'dob']);
            }
        }

        return true;
    }

    /**
     * Returns an anchor tag generated from a given value
     *
     * @param   string  $value  URL to use
     *
     * @return  mixed|string
     */
    public static function url($value)
    {
        if (empty($value)) {
            return HTMLHelper::_('users.value', $value);
        } else {
            // Convert website URL to utf8 for display
            $value = PunycodeHelper::urlToUTF8(htmlspecialchars($value));

            if (strpos($value, 'http') === 0) {
                return '<a href="' . $value . '">' . $value . '</a>';
            } else {
                return '<a href="http://' . $value . '">' . $value . '</a>';
            }
        }
    }

    /**
     * Returns html markup showing a date picker
     *
     * @param   string  $value  valid date string
     *
     * @return  mixed
     */
    public static function calendar($value)
    {
        if (empty($value)) {
            return HTMLHelper::_('users.value', $value);
        } else {
            return HTMLHelper::_('date', $value, null, null);
        }
    }

    /**
     * Returns the date of birth formatted and calculated using server timezone.
     *
     * @param   string  $value  valid date string
     *
     * @return  mixed
     */
    public static function dob($value)
    {
        if (!$value) {
            return '';
        }

        return HTMLHelper::_('date', $value, Text::_('DATE_FORMAT_LC1'), false);
    }

    /**
     * Return the translated strings yes or no depending on the value
     *
     * @param   boolean  $value  input value
     *
     * @return  string
     */
    public static function tos($value)
    {
        if ($value) {
            return Text::_('JYES');
        } else {
            return Text::_('JNO');
        }
    }

    /**
     * Adds additional fields to the user editing form
     *
     * @param   Form   $form  The form to be altered.
     * @param   mixed  $data  The associated data for the form.
     *
     * @return  boolean
     *
     * @since   1.6
     */
    public function onContentPrepareForm(Form $form, $data)
    {
        // Check we are manipulating a valid form.
        $name = $form->getName();

        if (!in_array($name, ['com_users.user', 'com_users.profile', 'com_users.registration'])) {
            return true;
        }

        // Add the registration fields to the form.
        FormHelper::addFieldPrefix('Joomla\\Plugin\\User\\Profile\\Field');
        FormHelper::addFormPath(__DIR__ . '/forms');
        $form->loadFile('customprofile');

        $fields = [
            'address1',
            'address2',
            'city',
            'region',
            'country',
            'postal_code',
            'phone',
            'website',
            'favoritebook',
            'aboutme',
            'dob',
            'tos',
        ];

        $tosArticle = $this->params->get('register_tos_article');
        $tosEnabled = $this->params->get('register-require_tos', 0);

        // We need to be in the registration form and field needs to be enabled
        if ($name !== 'com_users.registration' || !$tosEnabled) {
            // We only want the TOS in the registration form
            $form->removeField('tos', 'customprofile');
        } else {
            // Push the TOS article ID into the TOS field.
            $form->setFieldAttribute('tos', 'article', $tosArticle, 'customprofile');
        }

        foreach ($fields as $field) {
            // Case using the users manager in admin
            if ($name === 'com_users.user') {
                // Toggle whether the field is required.
                if ($this->params->get('profile-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('profile-require_' . $field) == 2) ? 'required' : '', 'customprofile');
                } elseif (
                    // Remove the field if it is disabled in registration and customprofile
                    $this->params->get('register-require_' . $field, 1) == 0
                    && $this->params->get('profile-require_' . $field, 1) == 0
                ) {
                    $form->removeField($field, 'customprofile');
                }
            } elseif ($name === 'com_users.registration') {
                // Case registration
                // Toggle whether the field is required.
                if ($this->params->get('register-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('register-require_' . $field) == 2) ? 'required' : '', 'customprofile');
                } else {
                    $form->removeField($field, 'customprofile');
                }
            } elseif ($name === 'com_users.profile') {
                // Case profile in site or admin
                // Toggle whether the field is required.
                if ($this->params->get('profile-require_' . $field, 1) > 0) {
                    $form->setFieldAttribute($field, 'required', ($this->params->get('profile-require_' . $field) == 2) ? 'required' : '', 'customprofile');
                } else {
                    $form->removeField($field, 'customprofile');
                }
            }
        }

        // Drop the customprofile form entirely if there aren't any fields to display.
        $remainingfields = $form->getGroup('customprofile');

        if (!count($remainingfields)) {
            $form->removeGroup('customprofile');
        }

        return true;
    }

    /**
     * Method is called before user data is stored in the database
     *
     * @param   array    $user   Holds the old user data.
     * @param   boolean  $isnew  True if a new user is stored.
     * @param   array    $data   Holds the new user data.
     *
     * @return  boolean
     *
     * @since   3.1
     * @throws  InvalidArgumentException on invalid date.
     */
    public function onUserBeforeSave($user, $isnew, $data)
    {
        // Check that the date is valid.
        if (!empty($data['customprofile']['dob'])) {
            try {
                $date = new Date($data['customprofile']['dob']);
                $this->date = $date->format('Y-m-d H:i:s');
            } catch (Exception $e) {
                // Throw an exception if date is not valid.
                throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_ERROR_INVALID_DOB'));
            }

            if (Date::getInstance('now') < $date) {
                // Throw an exception if dob is greater than now.
                throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_ERROR_INVALID_DOB_FUTURE_DATE'));
            }
        }

        // Check that the tos is checked if required ie only in registration from frontend.
        $task       = $this->app->input->getCmd('task');
        $option     = $this->app->input->getCmd('option');
        $tosEnabled = ($this->params->get('register-require_tos', 0) == 2);

        // Check that the tos is checked.
        if ($task === 'register' && $tosEnabled && $option === 'com_users' && !$data['customprofile']['tos']) {
            throw new InvalidArgumentException(Text::_('PLG_USER_PROFILE_FIELD_TOS_DESC_SITE'));
        }

        return true;
    }

    /**
     * Saves user customprofile data
     *
     * @param   array    $data    entered user data
     * @param   boolean  $isNew   true if this is a new user
     * @param   boolean  $result  true if saving the user worked
     * @param   string   $error   error message
     *
     * @return  void
     */
    public function onUserAfterSave($data, $isNew, $result, $error): void
    {
        $userId = ArrayHelper::getValue($data, 'id', 0, 'int');

        if ($userId && $result && isset($data['customprofile']) && count($data['customprofile'])) {
            $db = $this->db;

            // Sanitize the date
            if (!empty($data['customprofile']['dob'])) {
                $data['customprofile']['dob'] = $this->date;
            }

            $keys = array_keys($data['customprofile']);

            foreach ($keys as &$key) {
                $key = 'customprofile.' . $key;
            }

            $query = $db->getQuery(true)
                ->delete($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->whereIn($db->quoteName('profile_key'), $keys, ParameterType::STRING)
                ->bind(':userid', $userId, ParameterType::INTEGER);
            $db->setQuery($query);
            $db->execute();

            $query->clear()
                ->select($db->quoteName('ordering'))
                ->from($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->bind(':userid', $userId, ParameterType::INTEGER);
            $db->setQuery($query);
            $usedOrdering = $db->loadColumn();

            $order = 1;
            $query->clear()
                ->insert($db->quoteName('#__user_profiles'));

            foreach ($data['customprofile'] as $k => $v) {
                while (in_array($order, $usedOrdering)) {
                    $order++;
                }

                $query->values(
                    implode(
                        ',',
                        $query->bindArray(
                            [
                                $userId,
                                'customprofile.' . $k,
                                json_encode($v),
                                $order++,
                            ],
                            [
                                ParameterType::INTEGER,
                                ParameterType::STRING,
                                ParameterType::STRING,
                                ParameterType::INTEGER,
                            ]
                        )
                    )
                );
            }

            $db->setQuery($query);
            $db->execute();
        }
    }

    /**
     * Remove all user customprofile information for the given user ID
     *
     * Method is called after user data is deleted from the database
     *
     * @param   array    $user     Holds the user data
     * @param   boolean  $success  True if user was successfully stored in the database
     * @param   string   $msg      Message
     *
     * @return  void
     */
    public function onUserAfterDelete($user, $success, $msg): void
    {
        if (!$success) {
            return;
        }

        $userId = ArrayHelper::getValue($user, 'id', 0, 'int');

        if ($userId) {
            $db = $this->db;
            $query = $db->getQuery(true)
                ->delete($db->quoteName('#__user_profiles'))
                ->where($db->quoteName('user_id') . ' = :userid')
                ->where($db->quoteName('profile_key') . ' LIKE ' . $db->quote('customprofile.%'))
                ->bind(':userid', $userId, ParameterType::INTEGER);

            $db->setQuery($query);
            $db->execute();
        }
    }
}
1
  • That did it, thank you!
    – James
    Commented Dec 24, 2022 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.