Lập trình tạo một MVC Component đơn giản cho Joomla. Phần 3. Thêm một phần tử vào danh sách

Trong bài viết này

  1. Ý tưởng chung
  2. Logic hoạt động của chức năng thêm phần tử
  3. Xây dựng controller
  4. Xây dựng model
  5. Tạo view và layout
  6. Kết quả giai đoạn 1
  7. Bổ sung nút "Thêm sinh viên"
  8. Bổ sung nút "Lưu" và nút "Hủy"

Ý tưởng chung 🔝

Ở phần trước, ta đã xem xét việc xây dựng chức năng hiển thị danh sách sinh viên có trong cơ sở dữ liệu. Đây là chức năng mặc định, được thực thi khi trong truy vấn không có giá trị nào được thiết lập cho tham số 'task'. Và chức năng mặc định này được thực thi bởi controller mặc định, gọi đến view mặc định, sử dụng layout mặc định.
Ở phần này, ta sẽ xem xét việc xây dựng một chức năng mới: chức năng thêm một sinh viên vào danh sách. Và đây không phải là chức năng mặc định nên để thông báo cho component biết rằng người dùng muốn thực thi chức năng này thì ta cần thiết lập giá trị thích hợp cho tham số 'task' trong truy vấn HTTP. Như đã trình bày trước đây, giá trị của tham số 'task' có dạng 'controllername.methodname'. Chức năng mà ta đang xây dựng là THÊM một sinh viên vào danh sách thì phần 'methodname' nên được đặt là 'add'. Ta cần xây dựng một controller để phụ trách chức năng này. Tên của controller thường đặt theo tên của đối tượng quản lý ở dạng số ít hoặc số nhiều. Và chức năng ta đang xét là thêm MỘT sinh viên thì tên controller nên được chọn là tên đối tượng ở dạng số ít, tức là 'student'. Vậy, tóm lại, chúng ta cần xây dựng controller tên là 'student' (tên lớp đầy đủ là StudentsControllerStudent), trong đó có phương thức 'add()' để thêm một sinh viên vào danh sách (vào bảng trong CSDL). Và để yêu cầu chức năng này thì trong truy vấn, ta cần thiết lập cho tham số 'task' nhận giá trị là 'student.add'.
Để mọi người có thể hiểu rõ bản chất vấn đề, việc xây dựng chức năng này được thực hiện qua 2 giai đoạn. Ở giai đoạn thứ nhất, ta bỏ qua yếu tố giao diện mà chỉ quan tâm đến việc triển khai chức năng chính. Để kiểm tra kết quả, ta truyền tham số 'task' qua URL, tức là ta sẽ truy vấn tới địa chỉ index.php?option=com_students&task=student.add.
Sau khi hoàn thành giai đoạn một, ta bổ sung giao diện người dùng để có thể yêu cầu chức năng này một cách thuận tiện hơn. Cụ thể, trong giao diện mặc định (hiển thị danh sách sinh viên), ta thêm vào thanh công cụ nút "Thêm một sinh viên" để chỉ cần nhấn vào đó thì sẽ kích hoạt chức năng thêm sinh viên.

Logic hoạt động của chức năng thêm phần tử 🔝

Khi xây dựng component hiện thời, đối tượng mà chúng ta xem xét là sinh viên. Trước đó, chúng ta đã xem xét chức năng hiển thị danh sách các phần tử (mỗi sinh viên là một phần tử); bây giờ chúng ta đang xét chức năng thêm một phần tử; rồi sau này ta sẽ xét chức năng chỉnh sửa một phần tử. Dễ nhận thấy rằng các chức năng vừa được đề cập không chỉ áp dụng đối với sinh viên mà có thể áp dụng cho bất kỳ đối tượng nào khác (sách, nhân viên, mặt hàng,...). Và như vậy thì cũng chẳng có gì bất ngờ khi mà Joomla đã có sẵn controller để thực hiện các thao tác nói trên. Controller đó là JControllerForm (tức là FormController). Lớp này thừa kế từ lớp JControllerLegacy (tức là BaseController) và bổ sung thêm các phương thức: add(), batch(), edit(), save()cancel().
Việc thực thi chức năng thêm một phần tử sẽ diễn ra như sau:
  1. Client truy vấn phương thức add()
  2. Server chuyển hướng client tới index.php?option=com_students&view=student&layout=edit. Trang này phải hiển thị một form để người dùng nhập một phần tử. Dữ liệu form phải được submit tới địa chỉ index.php?option=com_students&task=student.save
  3. Người dùng submit dữ liệu, truy vấn phương thức save()
  4. Server kiểm tra dữ liệu. Nếu hợp lệ thì lưu vào CSDL rồi chuyển hướng client tới view mặc định; còn nếu không hợp lệ thì lưu dữ liệu vào session rồi thực thi bước 2.

Xây dựng controller 🔝

Trên cơ sở phân tích ở trên, ta xây dựng lớp controller StudentsControllerStudent (file student.php trong thư mục controllers) thừa kế lớp JControllerForm:
<?php
defined('_JEXEC') or die;
class StudentsControllerStudent extends JControllerForm
{
}
Chẳng lẽ chỉ có vậy thôi sao? Vâng! Chỉ có vậy! Bởi tất cả những phương thức ta cần đều đã được cài đặt trong lớp JControllerForm rồi.
Chẳng lẽ thế là xong rồi? Không! Tất nhiên là không! Dữ liệu của chúng ta sẽ được ghi vào CSDL bởi phương thức save(). Vậy hãy xem mã nguồn của phương thức này (file \libraries\src\MVC\Controller\FormController.php)
public function save($key = null, $urlVar = null)
{
    // Check for request forgeries.
    $this->checkToken();

    $app   = \JFactory::getApplication();
    $model = $this->getModel();
    $table = $model->getTable();
    $data  = $this->input->post->get('jform', array(), 'array');

    //...
    $form = $model->getForm($data, false);   //Dòng 695

    //...
    $validData = $model->validate($form, $data); //Dòng 713

    //....
    $model->save($validData)   //Dòng 754
}
Qua việc xem xét mã nguồn ta thấy 2 điều. Thứ nhất là chúng ta cần xây dựng một model mà có cài đặt các phương thức getTable(), getForm(), save(). Thứ hai là dữ liệu phải được submit qua phương thức POST ở dạng mảng có tên là 'jform'.

Xây dựng model 🔝

Trong số các lớp model của Joomla thì JModelAdmin là lớp được cài đặt sẵn các phương thức để làm việc với form: getForm(), validate(), cũng như có sẵn phương thức save(). Vì thế, ta sẽ xây dựng model (tên model trùng tên với view là 'student') thừa kế từ lớp này, đồng thời ta cần có phương án xử lý thích hợp để khi các phương thức getTable()getForm() được gọi thì sẽ trả về những đối tượng JTable JForm phù hợp với component của chúng ta.
<?php
defined('_JEXEC') or die;
class StudentsModelStudent extends JModelAdmin
{
    public function getForm($data = array(), $loadData = true)
    {
    }
}
Trong định nghĩa của lớp JModelForm thì getForm() là một phương thức trừu tượng (abstract) nên ta bắt buộc phải override nó khi xây dựng lớp thừa kế. Tạm thời, ta chỉ khai báo một phương thức rỗng để đảm bảo tuân thủ cú pháp, mã của nó sẽ được xem xét sau.

Phương án xử lý lời gọi $model->getTable()

Joomla có hỗ trợ lớp JTable để thao tác trên một bảng của CSDL. Tài liệu giới thiệu về lớp này có thể tham khảo ở đây: https://docs.joomla.org/Table_Basic_API_Guide. Trong mã của phương thức JFormController::save() ở trên, ta có thể đoán được rằng lời gọi $model->getTable() là để lấy được một đối tượng của lớp JTable ứng với bảng mà ta cần ghi dữ liệu vào. Phương thức getTable() được xây dựng trong lớp cơ sở JModelLegacy (tức là lớp BaseDatabaseModel). Qua việc đọc mã của phương thức này (file \libraries\src\MVC\Model\BaseDatabaseModel.php) ta thấy nó hoạt động như sau:
public function getTable($name = '', $prefix = 'Table', $options = array())
{
    if (empty($name))
    {
        $name = $this->getName();
    }

    if ($table = $this->_createTable($name, $prefix, $options))
    {
        return $table;
    }
}
Phương thức này sẽ tạo một bảng với $name$prefix được truyền qua tham số. Nếu không truyền $prefix thì giá trị mặc định là 'Table'. Nếu không truyền $name thì giá trị của nó được xác định là tên của model. Sau khi đã xác định được $name$prefix thì việc tạo bảng được thực hiện bằng lời gọi
BaseDatabaseModel::_createTable($name, $prefix, $options)
Và phương thức này lại gọi đến
JTable::getInstance($name, $prefix, $options)
Phương thức này sẽ tìm kiếm file '$name.php' rồi nạp lớp '$Prefix$Name'. Việc tìm kiếm file '$name.php' được thực hiện trong 2 thư mục là 'table' và 'tables' trong thư mục của component. Việc khởi tạo đối tượng của lớp được thực hiện qua lời gọi (ở dòng 312 file \libraries\src\Table\Table.php)
new $tableClass($db);
Trong trường hợp của chúng ta, model có tên là 'student' nên mặc định nó sẽ tìm kiếm file 'student.php' để nạp lớp có tên là 'TableStudent'. Cho nên, ta sẽ tạo thư mục '\com_students\tables' và trong đó tạo file 'student.php' để xây dựng lớp 'TableStudent' như sau:
<?php
defined('_JEXEC') or die;
class TableStudent extends JTable
{
    public function __construct($db)
    {
        parent::__construct('#__students', 'id', $db);
    }
}
Ở đây cần lưu ý rằng vì phương thức JTable::getInstance() khởi tạo đối tượng cho lớp của chúng ta bằng việc gọi đến contructor có 1 tham số là $db nên chúng ta cần phải định nghĩa constructor tương ứng trong lớp của mình. Và bởi vì lớp JTable đã hỗ trợ sẵn constructor có dạng
public function __construct($tableName, $primaryKeyName, $databaseObject)
cho nên constructor của chúng ta chỉ cần thực hiện việc đơn giản là gọi đến constructor của lớp cha với các tham số thích hợp. Cụ thể, tên bảng của ta là '#__students', trong bảng này thì primary key là trường 'id'.
Tóm lại, ta chỉ cần tạo lớp TableStudent đơn giản như trên là đủ để lời gọi $model->getTable() thu được đối tượng để làm việc với bảng '#__students' trong CSDL của chúng ta.

Phương án xử lý lời gọi $model->getForm

Joomla hỗ trợ lớp JForm để ta có thể làm việc một cách thuận tiện trên HTML form. Có thể tham khảo tài liệu về JForm ở đây: https://docs.joomla.org/Basic_form_guide.
Trong trường hợp của chúng ta, có thể hiểu rằng lời gọi $model->getForm() là để tạo ra đói tượng JForm phù hợp với một bản ghi thông tin về một phần tử (sinh viên). Như đã nói ở trên, trong lớp JModelAdmin thì getForm() là một phương thức trừu tượng nên chúng ta không thừa kế được gì từ lớp cha, và do đó phải xây dựng nó từ đầu.
Để làm việc với form thì trước hết cần phải xây dựng file XML để định nghĩa cấu trúc của form, tiếp đó sử dụng lớp JForm để nạp cấu trúc form, tạo ra đối tượng form rồi sau đó có thể sử dụng đối tượng này để thực hiện các thao tác khác nhau.
Hãy bắt đầu bằng việc tạo file XML để định nghĩa cấu trúc form. Ta có thể đặt tên bất kỳ cho nó, nhưng vì form này được sử dụng để thao thác dữ liệu về một sinh viên nên ta chọn tên là 'student.xml'. Ta có thể đặt file này ở bất kỳ vị trí nào trong component, nhưng bởi nó là form và gắn liền với model nên ta sẽ đặt nó trong thư mục 'forms' bên trong thư mục 'models'. Tóm lại, ta tạo ra file \com_students\models\forms\student.xml với nội dung như sau
<?xml version="1.0" encoding="utf-8" ?>
<form>
    <fieldset>
        <field name="id" type="hidden" default="0" />
        <field name="name" type="text" class="inputbox"
                size="40" label="Họ và tên" required="true"/>
        <field name="year" type="text" class="inputbox"
                size="40" label="Năm sinh" />
        <field name="avg" type="text" class="inputbox"
                size="40" label="Điểm trung bình" />
    </fieldset>
</form>
Ý nghĩa của file là khá rõ ràng, phù hợp với cấu trúc bản ghi sinh viên.
Giờ đây, sau khi đã có file định nghĩa cấu trúc form, ta hãy trở lại viết mã cho phương thức getForm() trong model của chúng ta như sau:
<?php
defined('_JEXEC') or die;
class StudentsModelStudent extends JModelAdmin
{
    public function getForm($data = array(), $loadData = true)
    {
        $form = JForm::getInstance('com_students.form.student',
            __DIR__.'/forms/student.xml',
            array('control'=>'jform'));
        if($loadData)
        {
            if(empty($data))
                $data = JFactory::getApplication()->getUserState("com_students.edit.student.data", array());
            if(empty($data))
                $data = $this->getItem();
            $form->bind($data);
        }
        return $form;
    }
}
Phương thức tĩnh JForm::getInstance() có cú pháp là:
public function getInstance($formId, $pathToFormXml, $options)
  • $formID là một chuỗi tùy ý, sao cho nó là duy nhất đối với mỗi form được định nghĩa trong ứng dụng Joomla. Vì thế, ở đây là lựa chọn một chuỗi chứa tên component của ta, kèm theo tên form.
  • $pathToFormXml là đường dẫn tới file XML chứa định nghĩa cấu trúc form
  • $options là một associative array chứa các tùy chọn. Hiện thời, phương thức này hỗ trợ duy nhất một option có tên là 'control'. Giá trị này (nếu có) sẽ là tên mảng để chứa các trường dữ liệu của HTML form khi submit. Và như đã lưu ý khi xem xét mã của phương thức JControllerForm::save() trên đây, ta cần thiết lập giá trị cho tùy chọn 'control' này là 'jform'.

Tạo view và layout 🔝

Như đã trình bày trong phần "Ý tưởng chung" trên đây, khi client yêu cầu chức năng add() thì server sẽ chuyển hướng tới index.php?option=com_students&view=student&layout=edit. Tại đây, người dùng nhập dữ liệu vào một form rồi submit tới chức năng save(). Như vậy, ta cần xây dựng view có tên là 'student' cùng với layout có tên là 'edit'.
Nội dung của file 'views/student/view.html.php'
<?php
defined("_JEXEC") or die;

class StudentsViewStudent extends JViewLegacy
{
    protected $form;
    public function display($tpl = null)
    {
        $this->form = $this->get('Form');
        JFactory::getApplication()->input->set('hidemainmenu', true);
        JToolbarHelper::title("Nhập thông tin sinh viên");
        return parent::display($tpl);
    }
}
Ta khai báo thuộc tính $form và gọi phương thức getForm() của model. Thuộc tính $form sẽ được layout sử dụng để xuất ra HTML form. Việc thiết lập tham số 'hidemainmenu'=true sẽ vô hiệu hóa menu quản trị chính; ý nghĩa của nó là: trong lúc người dùng đang xử lý form thì không được phép sử dụng menu chính.
Nội dung của tập tin layout 'views\student\tmpl\edit.php'
<?php
defined("_JEXEC") or die;
?>

<form action="<?php echo JRoute::_('index.php?option=com_students&task=student.save'); ?>" method="POST">
    <?php echo $this->form->renderField('id'); ?>
    <?php echo $this->form->renderField('name'); ?>
    <?php echo $this->form->renderField('year'); ?>
    <?php echo $this->form->renderField('avg'); ?>
    <?php echo JHtml::_('form.token'); ?>
    <button type="submit">Submit</button>
</form>
Ở đây, ta hiển thị một HTML form với phương thức POST, trong thuộc tính action ta thiết lập tham số task=student.save. Ý nghĩa của việc này đã được trình bày ở phần "Xây dựng controller" phía trên. Ngoài ra, trong phương thức JControllerForm::save(), Joomla cũng gọi checkToken() để chống lại tấn công CSRF nên khi tạo form ta cũng cần một trường để chứa token tương ứng.

Kết quả giai đoạn 1 🔝

Đến đây, ta đã hoàn tất giai đoạn 1. Hãy thực hiện truy vấn tới địa chỉ /index.php?option=com_student&task=student.add, ta sẽ được chuyển hướng tới địa chỉ /index.php?option=com_student&view=student&layout=edit với giao diện như sau
Khi nhập thông tin về một sinh viên mới rồi nhấn nút "Submit", ta sẽ truy vấn tới phương thức save() của controller 'student'. Tại đó, sau khi thông tin về sinh viên mới được bổ sung vào CSDL, ta sẽ được chuyển hướng tới view mặc định, và trong đó đã xuất hiện thêm dòng thông tin về sinh viên mới.

Bổ sung nút "Thêm sinh viên" 🔝

Đến đây, ta đã triển khai được chức năng "Thêm sinh viên" nhưng để truy cập được nó thì ta phải nhập URL một cách thủ công. Rõ ràng như vậy là không ổn, ta cần một giao diện người dùng thuận tiện hơn. Ý tưởng ban đầu sẽ như sau: người dùng truy cập vào component, view mặc định là hiển thị danh sách sinh viên. Tại đây ta sẽ đặt lên toolbar một nút "Thêm sinh viên" để khi nhấn vào thì kích hoạt chức năng thêm sinh viên. Để thực hiện đều này, ta cần chỉnh sửa view 'students' và layout 'default' của nó.
Nội dung file '\com_students\views\students\view.html.php'  được sửa lại như sau:
<?php
defined('_JEXEC') or die;

class StudentsViewStudents extends JViewLegacy
{
    protected $items;
    public function display($tpl = null)
    {
        $this->items = $this->get('Items');
        $this->addToolbar();
        return parent::display($tpl);
    }

    protected function addToolbar()
    {
        JToolbarHelper::title("Danh sách sinh viên");
        JToolbarHelper::addNew("student.add","Thêm một sinh viên");
    }
}
Có thể thấy sự thay đổi ở đây là rất ít. Ta bổ sung phương thức addToolbar() và gọi đến nó trong phương thức display(). Lớp JToolbarHelper hỗ trợ một số phương thức như sau:
  • title() để hiển thị dòng tiêu đề của toolbar
  • addNew() để thêm nút "Add" lên toolbar
  • editList() để thêm nút "Edit" lên toolbar
  • save() để thêm nút "Save" lên toolbar
  • cancel() để thêm nút "Cancel" lên toolbar
  • ....
Khi một nút được thêm lên toolbar thì nó cũng kèm theo mã javascript phù hợp với chức năng của từng nút. Ở đây, ta gọi phương thức addNew để hiển thị nút "Add" (nhưng có nhãn là "Thêm một sinh viên"); nút này sẽ thiết lập cho tham số 'task' của form nhận giá trị 'student.add' rồi submit form.
Cái gì??? Làm gì có form nào ở đây!!!
Vâng, đúng là không có, nhưng lại cần phải có. Tất cả các nút được tạo ra như trên đều đòi hỏi trên trang phải có một HTML form có thuộc tính 'id' với giá trị 'adminForm', và trong form phải có một trường (ẩn) có tên là 'task'. Khi ta nhấn một nút nào đó, đoạn mã javascirpt sẽ được kích hoạt và thực hiện:
  1. Tìm kiếm HTML form có id="adminForm"name="adminForm"
  2. Thiết lập cho tham số (trường) 'task' trong form nhận giá trị của tham số task khi tạo nút
  3. Thực hiện submit form
Như vậy, ta sẽ cần chỉnh sửa tập tin layout 'default.php' của view 'students' bằng cách thêm vào cuối mấy dòng như sau
<form action="<?php echo JRoute::_('index.php?option=com_students'); ?>"
    name="adminForm" id="adminForm" method="POST">
    <input type="hidden" name="task">
</form>
Sau sự điều chỉnh nhỏ nội dung của 2 file, khi truy cập vào component, ta có giao diện mới với nút "Thêm một sinh viên"
Và khi nhấn vào nút này, ta lại nhận được giao diện quen thuộc để thêm một sinh viên

Bổ sung nút "Lưu" và nút "Hủy" 🔝

Trong giao diện ở trên, ta có nút "Submit" để submit thông tin về sinh viên mới và không có nút nào để hủy thao tác thêm sinh viên này. Sẽ chuyên nghiệp hơn nếu ta thay một nút "Submit" như trên bằng hai nút: "Lưu" (save) để lưu submit thông tin về sinh viên mới và "Hủy" (cancel) để hủy bỏ thao tác thêm sinh viên. Ta thực hiện việc này bằng việc sửa đổi view 'student' và layout 'edit' của nó.
Nội dung file \com_students\views\student\view.html.php được sửa đổi như sau:
<?php
defined("_JEXEC") or die;
class StudentsViewStudent extends JViewLegacy
{
    protected $form;

    public function display($tpl = null)
    {
        $this->form = $this->get('Form');
        $this->addToolbar();
        return parent::display($tpl);
    }

    public function addToolbar()
    {
        JToolbarHelper::title("Nhập thông tin sinh viên");
        JToolbarHelper::save("student.save","Lưu");
        JToolbarHelper::cancel("student.cancel","Hủy");
    }
}
Có thể thấy, sự chỉnh sửa ở đây là thêm vào 2 dòng để đặt 2 nút "Lưu" và "Hủy" lên toolbar.
Khác với trường hợp trên, ở đây, trong layout ta đã có sẵn một form rồi nên thay vì tạo thêm form mới thì ta sẽ chỉnh sửa form trong layout. Nội dung của layout 'edit.php' được sửa lại như sau:
<?php
defined("_JEXEC") or die;
?>

<form action="<?php echo JRoute::_('index.php?option=com_students'); ?>"
      id="adminForm" method="POST">
    <?php echo $this->form->renderField('name'); ?>
    <?php echo $this->form->renderField('year'); ?>
    <?php echo $this->form->renderField('avg'); ?>
    <?php echo JHtml::_('form.token'); ?>
    <input type="hidden" name="task" />
</form>
Các thay đổi trên form bao gồm:
  • thuộc tính action được sửa để bỏ tham số task
  • thêm thuộc tính id="adminForm"
  • bỏ nút submit
  • thêm trường ẩn có name="task"
Sau khi thay đổi, ta truy cập lại chức năng "Thêm một sinh viên" thì sẽ có được giao diện mới như sau:
Nếu nhập thông tin rồi nhấn nút "Lưu" thì thông tin về sinh viên mới sẽ được lưu vào CSDL rồi ta sẽ được chuyển hướng tới view mặc định. Còn nếu ta bấm nút "Hủy" thì thông tin sẽ không được lưu, chỉ có việc chuyển hướng đến view mặc định.
Ở phần trên ta đã giải thích hoạt động của phương thức add()save() của JControllerForm. Khi đã hiểu được vấn đề đó thì không khó khăn để hình dung hoạt động của phương thức cancel(). Nó chỉ đơn giản là xác định view và layout mặc định rồi thực hiện chuyển hướng.
Đến đây ta đã hoàn thành việc xây dựng chức năng "Thêm sinh viên".

Comments

Popular posts from this blog

Cài đặt Xdebug cho VSCode trên Windows

Lập trình tạo một MVC Component đơn giản cho Joomla. Phần 4. Chỉnh sửa một phần tử

Lập trình tạo một MVC Component đơn giản cho Joomla. Phần 1. Khởi tạo component