0

I am creating a new Joomla J4 component for my website. I have previously created components in J3 using Akeeba's Framework on Framework. For J4 I'm working to learn how to create a J4 MVC native component and I'm stuck on this issue. All I want to do is to add fields to a record to track when a record was last modified and who did the modification. (Eventually, I'd like to make use of the record lock functionality as well but that's not part of this question.)

It seems to me that the modified_on and modified_by field values should be updated when the JFORM is being saved (store?), but I don't understand where that happens in the MVC framework.

Can someone explain to me where I need to add the code to make this happen? Or, point me in a difference direction regarding to to make this work? Thanks in advance for your assistance!

For reference, here's my code for this view.

These are the fields I have configured in my db table:

**#    Name           Type            Null    Default**
  11     modified_by    bigint          Yes     NULL
  12     modified_on    datetime        Yes     NULL

This is the JFORM I have setup `

<fieldset name="core">
    <field
        name="title"
        type="text"
        label="Client"
        required="true"
        maxlength="100"
    />

    <field
        name="description"
        type="textarea"
        label="Description"
        description=""
        required="false"
        maxlength="300"
    />

    <field
        name="portal_name"
        type="text"
        label="Portal Name"
        description="Enter the URL for the client portal"
        required="true"
        maxlength="100"
    />
    <field
        name="primary_contact"
        type="text"
        label="Primary Contact"
        description=""
        required="false"
        maxlength="100"
    />

    <field
        name="email_address"
        type="text"
        label="Email"
        description=""
        required="false"
        maxlength="100"
    />

    <field
        name="modified_by"
        label="JGLOBAL_FIELD_MODIFIED_BY_LABEL"
        class="readonly"
        readonly="true"
        filter="unset" />
    
    <field
        name="modified_on"
        type="calendar"
        label="Last Modified"
        default="NOW"
        class="readonly"
        showtime="true"
        translateformat="true"
        format="%d-%m-%Y %h:%i:%s"
        readonly="true"
        filter="unset"  
    />



    <field
        name="id"
        type="text"
        label="Client ID"
        class="readonly"
        default="0"
        readonly="true"
    />

    <field
        name="state"
        type="list"
        label="Status"
        class="form-select-color-state"
        default="1"
        validate="options"
        >
        <option value="1">JPUBLISHED</option>
        <option value="0">JUNPUBLISHED</option>
        <option value="2">JARCHIVED</option>
        <option value="-2">JTRASHED</option>
    </field>
`

These are the functions in the Model for the page:

namespace Bluewater\Component\bwivisuals\Administrator\Model;

\defined('_JEXEC') or die;


use Joomla\CMS\Date\Date;
use Joomla\CMS\Event\AbstractEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Filter\InputFilter;
use Joomla\CMS\Filter\OutputFilter;
use Joomla\CMS\Form\Form;
use Joomla\CMS\Form\FormFactoryInterface;
use Joomla\CMS\Helper\TagsHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\LanguageHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\AdminModel;
use Joomla\CMS\MVC\Model\WorkflowBehaviorTrait;
use Joomla\CMS\MVC\Model\WorkflowModelInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\String\PunycodeHelper;
use Joomla\CMS\Table\TableInterface;
use Joomla\CMS\Tag\TaggableTableInterface;
use Joomla\CMS\UCM\UCMType;
use Joomla\CMS\Versioning\VersionableModelTrait;
use Joomla\CMS\Workflow\Workflow;
use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper;
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;
use Joomla\Utilities\ArrayHelper;

/**
 * Item Model for a Country.
 *
 * @since  1.6
 */

class ClientModel extends AdminModel
{
    /**
     * The prefix to use with controller messages.
     *
     * @var    string
     * @since  1.6
     */
    protected $text_prefix = 'COM_BWIVISUALS_CLIENT';

    /**
     * Method to get the record form.
     *
     * @param   array    $data      Data for the form.
     * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
     *
     * @return  Form|boolean  A Form object on success, false on failure
     *
     * @since   1.6
     */
    public function getForm($data = array(), $loadData = true)
    {
        // Get the form.
        $form = $this->loadForm('com_bwivisuals.client', 'client', array('control' => 'jform', 'load_data' => $loadData));

        if (empty($form))
        {
            return false;
        }

        return $form;
    }

    /**
     * Method to get the data that should be injected in the form.
     *
     * @return  mixed  The data for the form.
     *
     * @since   1.6
     */
    protected function loadFormData()
    {
        // Check the session for previously entered form data.
        $app = Factory::getApplication();
        $data = $app->getUserState('com_bwivisuals.edit.client.data', array());

        if (empty($data))
        {
            $data = $this->getItem();

        }

        return $data;
    }

    /**
     * Method to get a table object, load it if necessary.
     *
     * @param   string  $name     The table name. Optional.
     * @param   string  $prefix   The class prefix. Optional.
     * @param   array   $options  Configuration array for model. Optional.
     *
     * @return  Table  A Table object
     *
     * @since   3.0
     * @throws  \Exception
     */
    public function getTable($name = '', $prefix = '', $options = array())
    {
        $name = 'Clients';
        $prefix = 'Table';

        if ($table = $this->_createTable($name, $prefix, $options))
        {
            return $table;
        }

        throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_TABLE_NAME_NOT_SUPPORTED', $name), 0);
    }
}

And this is the layout file, edit.php

\defined('_JEXEC') or die;

use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;


HTMLHelper::_('behavior.formvalidator');
HTMLHelper::_('behavior.keepalive');

?>

<form action="<?php echo Route::_('index.php?option=com_mycomponents&view=client&layout=edit&id=' . (int) $this->item->id); ?>"
    method="post" name="adminForm" id="client-form" class="form-validate">

<?php echo LayoutHelper::render('joomla.edit.title_alias', $this); ?>

    <div>
        <?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', array('active' => 'details')); ?>

        <?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('Details')); ?>
        <div class="row">
            <div class="col-md-9">
                <div class="row">
                    <div class="col-md-6">
                        <?php echo $this->form->renderField('description'); ?>
                        <?php echo $this->form->renderField('portal_name'); ?>
                        <?php echo $this->form->renderField('primary_contact'); ?>
                        <?php echo $this->form->renderField('email_address'); ?>
                        <?php echo $this->form->renderField('modified_by'); ?>
                        <?php echo $this->form->renderField('modified_on'); ?>
                        <?php echo $this->form->renderField('id'); ?>
                    </div>
                </div>
            </div>
            <div class="col-md-3">
                <div class="card card-light">
                    <div class="card-body">
                        <?php echo LayoutHelper::render('joomla.edit.global', $this); ?>
                    </div>
                </div>
            </div>
        </div>
        <?php echo HTMLHelper::_('uitab.endTab'); ?>

        <?php echo HTMLHelper::_('uitab.endTabSet'); ?>
    </div>
    <input type="hidden" name="task" value="">
    <?php echo HTMLHelper::_('form.token'); ?>
</form>

And this is the ClientController file:

namespace Bluewater\Component\bwivisuals\Administrator\Controller;

defined('_JEXEC') or die;

use Joomla\CMS\MVC\Controller\FormController;

/**
 * Controller for a single record
 *
 * @since  1.6
 */
class ClientController extends FormController
{
}

This is the ClientsTable

namespace Bluewater\Component\bwivisuals\Administrator\Table;

\defined('_JEXEC') or die;

use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;

/**
 * Featured Table class.
 *
 * @since  1.6
 */
class ClientsTable extends Table
{
    /**
     * Constructor
     *
     * @param   DatabaseDriver  $db  Database connector object
     *
     * @since   1.6
     */
    public function __construct(DatabaseDriver $db)
    {
        parent::__construct('#__bwivisuals_clients', 'id', $db);

        $this->setColumnAlias('published', 'state');
    }
}

And I tried creating a ClientTable, but that didn't work

namespace Bluewater\Component\bwivisuals\Administrator\Table;

\defined('_JEXEC') or die;

use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;

/**
 * Featured Table class.
 *
 * @since  1.6
 */
class ClientTable extends Table
{
    /**
     * Constructor
     *
     * @param   DatabaseDriver  $db  Database connector object
     *
     * @since   1.6
     */
    public function store($updateNulls = false) {
    $this->modified_by = Factory::getApplication()->getIdentity()->id;
    $this->modified_on = date('Y-m-d H:i:s');
    
    return parent::store($updateNulls);
}
}

2 Answers 2

2

With the default implementations, that is using classes that inherit from FormController and FormModel, your form data will be submitted to your controller, passed to your model first and then passed to your Table class to perform the actual save.

I would suggest that the correct place to put code for your specific requirement should be within the store() method of your Table class:

public function store($updateNulls = false) {
    $this->modified_by = Factory::getApplication()->getIdentity()->id;
    $this->modified_on = date('Y-m-d H:i:s');
    
    return parent::store($updateNulls);
}

This is the last point before the record is actually written to the database, using the inherited methods, so should catch writes from anywhere in the application. However, it is still possible to write the record directly to the database without using the Table methods, which would bypass the modified fields and not cause them to be updated. This is a design consideration when writing your component and any code associated with the database table.

7
  • Thanks Matt. If I understand your suggestion correctly, I would need to extend the Table class by creating a ClientTable.php file and then include the method code for the you suggested above. Is that correct? I'm not I understand how to make use of the inheritance, but I do learn quickly.
    – mb87
    Jul 1 at 20:00
  • Yes, that's correct, but you should already have a ClientTable, it's being referenced from your model's getTable() method, Jul 1 at 23:09
  • I have a ClientsTable, but not a ClientTable. The viewing I'm working on has an AdminModel, not a ListModel. I created a ClientTable and I added your suggested code. I have added the code to my original question. Do I need to copy all the code from the original public function store method to the ClientTable code above and then add in the two extra lines you suggested? What am I not understanding? Thanks for your patience with me as I am learning. Thanks.
    – mb87
    Jul 2 at 3:16
  • Sorry, that was my mistake - I didn't notice the 's' on the end of your Table name. Add the additional code to the ClientsTable. No, you don't need to add the code from Table::store(), the code that you're adding performs the task that you want and then calls parent::store(), which runs all the code from the original (parent) class without needing to enter it separately Jul 2 at 6:57
  • 1
    @GlennArkell - I think there's an element of personal choice, both methods will work. I would normally use bind() as well, but specifically for the modified by/date fields I use store() just because it feels right to store the update time as the time it was written to the database, rather than the time it was bound. Practically though, the time is probably going to be the same whichever method is used, as they are executed so close together. Oct 2 at 7:52
0

The cut and paste of the last window of code, the view template, is missing the first part of the element so I am not sure what you have got there and what you haven't however you normally need to call a method in the controller when the form is submitted.

It can be in the action argument of the element

<form action="<?php echo Route::_('index.php?option=com_componentname&task=controlername.methodname'); ?>">

or you can do it as part of the element which I can see you have in your example.

<input type="hidden" name="task" value="controlername.methodname">

Also you can do it via a toolbar button in the back-end and if you have used Toolbars in the front end it works the same.

$this->toolbar->standardButton('Save')
            ->task('controlername.methodname')

or the slightly different example used by a lot of the core features toolbars

$this->toolbar->save('controlername.methodname');

Provided that your Controller extends FormController, eg. class BannerController extends FormController,then there is already a default save() method available to you. Thus in the examples above the task or action would be controllername.save. Or you may want to do some other processing first before you call the save() method.

The first parts of the default save() method of FormController gives you some clues as to what is happening.

    public function save($key = null, $urlVar = null)
    {
        // Check for request forgeries.
        $this->checkToken();

        $app     = $this->app;
        $model   = $this->getModel();
        $table   = $model->getTable();
        $data    = $this->input->post->get('jform', [], 'array');
....

The $data gets the field values from your Form via the Input while the $model is the path to $table and it is your Table class that is going to do the actual saving of the data and some basic validation etc. Have a look at ..\libraries\src\Table\Table.php for more detail.

3
  • Thanks for your response. The first thing I recognize is I had a lot to learn about using the MVC framework. The frontend modules I've worked on are very different. Here's the action line from my original post: <form action="<?php echo Route::_('index.php?option=com_component&view=client&layout=edit&id=' . (int) $this->item->id); ?>" method="post" name="adminForm" id="client-form" class="form-validate"> The Controller I created does extend FormController. However, it is empty. I haven't added any code to it. Does that mean it is using the default FormController methods?
    – mb87
    Jul 1 at 19:43
  • With inheritance, if you define a class as 'extends another class', your class will automatically have access to all the methods and properties of the class you're extending, which allows you to override individual methods without having to rewrite all the others. So, yes, your empty Controller that extends FormController will have all the properties, methods and functionality of FormController Jul 1 at 23:12
  • The action= in your above comment is just going to re-display the same page/view thus you need to add the &task=controller.name to the <input> element, use a ToolBar button or change your action= to include the &task=controller.name . Understanding how to structure your extensions using the MVC approach is worth the effort and will help you in the long run, however keep in mind Joomla has its own interpretation of the MVC approach to things, as do other frameworks, and not all extensions claiming to follow or use the Joomla MVC are really following it properly. Learn and confirm.:)
    – Irata
    Jul 2 at 9:22

Your Answer

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

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