admin | ||
.editorconfig | ||
CODING_STANDARDS.md | ||
depot.xml | ||
LICENSE.md | ||
README.md |
Depot
Introduction
This project is also based on the desperation to sort and find my electronic components. Some integrated circuits (ICs) accumulated in various boxes and the question of whether I own that or the other IC, had to be painstakingly researched. Above all, there were at one time several places where components were stored or partly kept at a plant. The idea was born, but it took long time to implement it. Klosterneuburg, October 2023 Thomas Kuschel KW4NZ.
Workflow (since 0.0.1)
In git we start to make a new branch named b1_basic_backend, where we start developing our project. The first run with simply renaming entries from a copy of the component com_banners did not work as expected. So let us start at the very beginning:
Adding basic files for component (b1_basic_backend)
With the git branch b1_basic_backend Add the following basic six files:
-
admin
- src/Extension/DepotComponent.php: The main extension file for the component.
- services/provider.php: It tells Joomla how to initialize or boot the component.
- src/Controller/DisplayController.php: The default Controller for the component.
- src/View/Parts/HtmlView.php: The Html View for the "Parts" page.
- tmpl/parts/default.php: The layout file for the "Parts" page.
-
depot.xml: XML manifest file that tells Joomla! how to install the component.
Description of each file:
1. DepotComponent.php
This file contains class for the extension. The class extends MVCComponent. Compare this version with the original at Tech Fry Tutorium.
namespace KW4NZ\Component\Depot\Administrator\Extension;
use Joomla\CMS\Extension\MVCComponent;
class DepotComponent extends MVCComponent
{
}
2. provider.php
This is a special file that tells Joomla how to initialize the component - which services it requires and how they should be provided.
The service provider file registers dependencies the component will use. Here, we have included two dependencies:
-
DispatcherFactory is needed to create the Dispatcher class instance, and then Joomla will call dispatch() on this Dispatcher object, as the next step in running the component.
-
MVCFactory is needed to create the Controller, View, Model and Table class instances on behalf of the component.
3. DisplayController.php
This is a default controller for the component. It simply sets its default view and leaves the rest to its parent.
When you view the component through URL, Joomla uses the controller to execute the task. The task is the name of method in the controller file. If you do not pass the controller or task in the URL, it defaults to Display Controller and display Task.
The default view is the name of the component. So, here we need to override the
default view to parts
.
4. HtmlView.php
This file contains class HtmlView that extends BaseHtmlView. The BaseHtmlView is the base class for a Joomla View.
The view gets the data from the model to be output by the layout file.
For example:
$this->msg = $this->get('Msg');
This method converts the get('Msg') call into a getMsg() call on the model, which is the method which you have to provide in the model.
The view file displays data using the template layout file - $tpl, which defaults to default.php.
5. default.php
This file holds the template for the page. When no specific layout is requested for a view, Joomla will load the template in the default.php file.
<h2>Welcome to Depot Component!</h2>
6. depot.xml
This file tells Joomla how to install the component and what file are included.
In the administration part, we include a link to the menu and include files and folders (services, src, tmpl and so on) which are in the parent folder admin of the component. While installing the component, these will get copied to the Joomla administrator/components/com_depot.
Installation the component
Create a .zip file of the com_depot directory. Then inside the Joomla Administration upload this .zip package.
Now you should see a new link "Depot" in the "Compnents" section of the menu. If you click it, you should see the default "Depot" page.
Language files
We create two language files for the system and the component Depot at the directory /admin/language/en-GB/ naming it
- com_depot.ini
- com_depot.sys.ini
Creating and managing Joomla database (b2_database)
With the new git branch b2_database we continue our workflow.
Joomla usually manage its content with a database. In our component, we use a MariaDB database. At the time of development of this component we have PHP8.2, and we use Mariadb database with version from 11.1, client 15.2 for Linux (x86_64) using readline 5.1 We support MySQL and MariaDB; not tested support for PostgreSQL.
In the manifest file <component_name>.xml, here the file depot.xml, there are installation instructions to "install", "uninstall", and "update" the Joomla extension/component.
The SQL plain text files are stored in and as:
-
admin/sql/
- install.mysql.utf8.sql
- uninstall.mysql.utf8.sql
-
admin/sql/updates/mysql/
- 0.0.1.sql
- 0.0.4.sql
- 0.0.5.sql
Database table installation
- When the component is installed for the first time, the file install.mysql.utf8.sql is executed.
- If the component is already installed, the update
scenario comes into play, the folder admin/sql/updates/mysql/: Only the files with higher version
numbers than the installed version of the component will be
executed in ascending order.
Hint: The version of the installed component is stored in the Joomla's table "#__schemas".
The "#__"-prefix is substituted automatically with the database prefix, (e.g. "jm_") which is defined in the configuration file (configuration.php) of the installed Joomla version as parameter $dbprefix.
install.mysql.utf8.sql
DROP TABLE IF EXISTS `#__depot`;
CREATE TABLE `#__depot`(
`id` SERIAL,
`component_name` VARCHAR(1024) CHARACTER SET ascii COLLATE ascii_general_ci NULL DEFAULT NULL
COMMENT 'unique component name (ASCII characters only)',
`alias` VARCHAR(1024) NOT NULL DEFAULT '',
`description` VARCHAR(4000) NOT NULL DEFAULT '',
`quantity` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`quantity_exp` INT(11) NOT NULL DEFAULT 0
COMMENT 'Exponent of the quantity (10^x of the number, usually 0 i.e. 10⁰)',
`asset_id` INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'FK to the #__assets table.',
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`created_by` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`checked_out` INT(11) NOT NULL DEFAULT 0,
`checked_out_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`modified_by` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`path` VARCHAR(400) NOT NULL DEFAULT '',
`state` TINYINT(4) NOT NULL DEFAULT 0
COMMENT 'Published=1,Unpublished=0,Archived=2,Trashed=-2',
`access` TINYINT(4) NOT NULL DEFAULT 0,
`params` VARCHAR(1024) NOT NULL DEFAULT '',
`image` VARCHAR(1024) NOT NULL DEFAULT '',
`ordering` INT(11) NOT NULL DEFAULT 0,
`version` int unsigned NOT NULL DEFAULT 1,
-- references to other tables:
`category_id` INT(11) NOT NULL DEFAULT 0,
`datasheet_id` INT(11) NOT NULL DEFAULT 0,
`datasheet_alt` VARCHAR(1024) NOT NULL DEFAULT '',
`manufacturer_id` INT(11) NOT NULL DEFAULT 0,
`stock_id` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_state` (`state`),
KEY `idx_stock_id` (`stock_id`),
KEY `idx_manufacturer` (`manufacturer_id`),
UNIQUE KEY `aliasindex` (`alias`,`manufacturer_id`,`stock_id`)
) ENGINE=InnoDB
AUTO_INCREMENT=0
DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `#__depot` (`component_name`,`alias`,`description`,`quantity`,`created`,
`ordering`,`state`,`manufacturer_id`) VALUES
('1N5404','1n5404','diode, rectifier 3A',9,'2023-09-25 15:00:00',1,1,1),
('1N4148','1n4148','diode, general purpose',1234,'2023-09-25 15:15:15',2,1,2);
The table is created in the database when the component is installed. Also two lines of sample data are inserted into the table "#__depot".
Database table uninstallation
When the component is uninstalled the uninstall.mysql.utf8 sql is executed. We drop the used tables from the database.
install.mysql.utf8.sql
DROP TABLE IF EXISTS `#__depot`;
Database table update
When the component is updated the admin/sql/updates/mysql folder with its files is executed.
Even if you do not need a database update, you can add an empty file in admin/sql/updates/mysql/0.0.2.sql to initialize the schema version.
In future versions, if you plan to use database tables, the update can be performed automatically. We create an empty file, just with a comment "-- version 0.0.2" admin/sql/updates/mysql/0.0.2.sql
-- version 0.0.2
Manifest file for extensions
The sql files are only executed if they exist in the <component_name>.xml manifest file. We add this after the namespace tag, just before the files information tag:
<install> <!-- Runs on install -->
<sql>
<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
</sql>
</install>
<uninstall> <!-- Runs on uninstall -->
<sql>
<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
</sql>
</uninstall>
<update>
<schemas>
<schemapath type="mysql">sql/updates/mysql</schemapath>
</schemas>
</update>
The <version> tag of the manifest must be updated to 0.0.2 and also add the sql folder in the administration files section.
Table class
src/table/DepotTable.php
For each database table, you have to define a table class. The model asks the table to get information or perform database operations.
This table class has to be defined in admin/src/Table/DepotTable.php file.
<?php
namespace KW4NZ\Component\Depot\Administrator\Table;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;
\defined('_JEXEC') or die;
class DepotTable extends Table
{
function __contruct(DatabaseDriver $db)
{
parent::__contruct('#__depot', 'id', $db);
}
}
Joomla uses the Table Object to get item or record, insert records, update or delete records for the database operations.
Methods in the table objects are:
- load() to load the existing record from the database, passing the primary key of the record.
- bind() to set the new values for the fields.
- check() to perform any validation.
- store() to save the new values to the database.
- delete() to delete the record from the database.
Get a Form in Joomla component (b3_form)
The Form class of Joomla is used to create complex forms with flexible layouts and dynamic properties. First, the form fields are defined in the XML file. Then the view file gets the form from the model and layout file displays the form.
XML Form file
admin/forms/part.xml
<?xml version="1.0" encoding="UTF-8"?>
<form>
<field
name="title"
type="text"
label="JGLOBAL_TITLE"
required="true"
/>
</form>
Similarly you can add other fields to the XML file.
View file
admin/src/View/Part/HtmlView.php
This file is similar to the view file added earlier for the "Parts" view. The view file gets the form from the model in the display() method.
<?php
namespace KW4NZ\Component\Depot\Administrator\View\Part;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
\defined('_JEDEC') or die;
class HtmlView extends BaseHtmlView
{
protected $form;
protected $item;
public function display($tpl = null)
{
$this->form = $this->get('Form');
$this->item = $this->get('Item');
parent::display($tpl);
}
}
The code will look for the method getForm() and getItem() in the model file. We need to get the item to get the ID of the record in case of editing the existing record.
Model file
admin/src/Model/PartModel.php
We need to extend the model class with the AdminModel. The AdminModel class extends FormModel class. The getForm() method gets the Form object for the edit form.
namespace KW4NZ\Component\Depot\Administrator\Model;
use Joomla\CMS\MVC\Model\AdminModel;
\defined('_JEXEC') or die;
class PartModel extends AdminModel
{
public function getForm($data = array(), $loadData = true)
{
$form = $this->loadForm('com_depot.part',
'part', array('control' => 'jform',
'load_data' => $loadData));
if (empty($form)) {
return false;
}
return $form;
}
}
The loadForm() and preprocessForm() methods are defined in the FromModel class and the bind() method is defined in the Form class. The first argument (name) of loadForm() is set to "com_depot.part". The second argument (form xml source) is "part", and the third argument is the associative array for options.
The form is defined in the source file: forms/part.xml
After you have set the $form variable with the Form object, you check to see if you are loading data to the form. If you want to pre-load data for the form, you include an element called "load_data" that is set to a boolean true. Then, the method calls the loadFormData() method to get the data for the form. This method gets any previously loaded data from the session or database.
Modifying Form dynamically
Inside the getForm() method, before returning the $form, you can modify the form with many methods of the Form class. You can easily fine-tune your forms dynamically before they are rendered.
Layout file - rendering Form
tmpl/part/edit.php
After you get the form in the view file ($this->form), the form is rendered in the layout file (edit.php).
<?php
use Joomla\CMS\HTML\HTMLHelper;
$wa = $this->document->getWebAssetManager();
$wa->useScript('keepalive');
$wa->useScript('form.validate');
?>
<form action="<?= Route::_('index.php?option=com_depot&layout=edit&id=' .
(int) $this->item->id); ?>"
method="post" name="adminForm" id="item-form" class="form-validate">
<?= $this->form->renderField('title'); ?>
<input type="hidden" name="task" value="part.edit" />
<?= HTMLHelper::_('form.token'); ?>
</form>
The form validate script is required to submit the for. The renderField() method of the Form class displays the field - both label and input.
We can access the form using following URL:
administrator/index.php?option=com_depot&view=part&layout=edit
It displays edit.php layout file of the Part View. When the form is submitted, the data is sent to the controller depending upon the action buttons in the toolbar.
Please remember to add the additional folder form
to the Manifest file "depot.xml", i.e.
just a line before <folder>services</folder>
:
<folder>form</folder>
Adding administrator's actions (Back-end) - Save and Cancel (b4_actions)
In the form View, you can add action buttons like "Save" and "Cancel" to submit the form or cancel the editing respectively. These buttons are added to the toolbar.
These buttons require compound tasks (controller and method). For example,
- Save and Edit: part.apply
- Save and Close: part.save
- Cancel: part.cancel
View file
admin/src/View/Part/HtmlView.php
In the View file, we create a new method to add a toolbar: addToolbar()
. The
toolbar hides the sidebar menu on forms, sets the title and adds action buttons.
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\Toolbar;
use Joomla\CMS\Toolbar\ToolbarHelper;
protected function addToolbar()
{
Factory::getApplication()->getInput()->set('hidemainmenu', true);
ToolbarHelper::title('Part: Add');
ToolbarHelper::apply('part.apply');
ToolbarHelper::save('part.save');
ToolbarHelper::cancel('part.cancel', 'JTOOLBAR_CLOSE');
}
The display() calls this method to include the toolbar.
public function display($tpl = null)
{
$this->form = $this->get('Form');
$this->addToolbar();
parent::display($tpl);
}
Controller file
admin/src/Controller/PartController.php* Now, when these action buttons are clicked, Joomla will look for apply(), save() or cancel() methods in the Part controller. So, create a controller clss that will extend FormController. These methods are already defined in the parent class.
use Joomla\CMS\MVC\Controller\FormController;
class PartController extends FormController
{
}
Model file
admin/src/model/PartModel.php
If you click on Save button, it will save the data and redirect to the editing screen. However, the data will not be present in the form. Though, we have set $loadData
to true, we also need to create a method loadFormData()
to get the data for the form.
protected function loadFormData()
{
$app = Factory::getApplication();
$data = $app->getUserState('com_depot.edit.part.data', []);
if (empty($data)) {
$data = $this->getItem();
}
return $data;
}
First, this method tries to get the data from the session. If it fails to get data from the
session, then it gets data from the database. The getItem()
method is defined in the
parent class.
Suppose the user has filled out the form to add a new item but has some invalid data in the form. In this case, the save will not be successful, so the data is not saved in the database. It would be very frustrating if the user hat to reenter all the fields to fix a single error. To handle this scenario, you save the entered data in the user's session. Only after the save is successful, you clear this out of the session.
So, you either have data from the database, which you can get with getItem()
, or you have
data from the session, which you can get with getUserSate()
.
Automatic handling of fields (b5_field_manipulation)
Values of some fields in the form can be automatically handled. There is no need to fill in the values while creating or editing a record.
For example, alias
can be generated from the title
(or here, from the component's name),
dates can be set to the current date or to null
, user id can be obtained from current
logged in user. Further may you also need to check validity of some fields like "should not
be empty", alias
should be unique, etc.
The data submitted by the form needs to be modified before saving. This can be done at various places:
- in the Model class by oeverriding the
save()
method orpreparetable()
method. - in the Table class by overriding the
bind()
,check()
, orstore()
methods.
1. Model file
**admin/src/Model/PartModel.php
public function save($data)
{
/* Add code to modify data before saving */
return parent:save($data);
}
It is better to perform automatic handling of fields in the Table class as the data can be saved not only from administration, but also from frontend, API or by any other means.
Example generating Alias
Alias is generated from the component_name
(or any other field) using OutputFilter class
method.
if (empty($data['alias']))
{
if (Factory::getConfig()->get('unicodeslugs') == 1) {
$data['alias'] = OutputFilter::stringURLUnicodeSlug($data['component_name']);
} else {
$data['alias'] = OutputFilter::stringURLSafe($data['component_name']);
}
}
Ordering
The ordering value is calculated by finding the maximum value in the column and then incrementing it.
/* if it is 0 -> get the max + 1 value */
if (!$data['ordering']) {
$db = Factory::getDbo();
// $query = $db->getQuery(true) // is deprecated,
// using createQuery() instead:
$query = $db->createQuery()
->select('MAX(ordering)')
->from('#__depot');
$db->setQuery($query);
$max = $db->loadResult();
$data['ordering'] = $max + 1;
}
2. bind()
The bind()
splits the article text or description into intro text and full text based on read
more tag in the content. This method also converts fields data from arrays to JSON for
saving into the database.
public function bind($array, $ignore = '')
{
if (isset($array['attribs']) && \is_array($array['attribs'])) {
$registry = new Registry($array['attribs']);
$array['attribs'] = (string) $registry;
}
return parent::bind($array, $ignore);
}
3. check()
The check() checks whether title of the article is not empty. This method also sets the alias, hits, publishing dates.
public function check()
{
try {
parent::check();
}
catch (\Exception $e) {
$this->setError($e->getMessage());
return false;
}
if (trim($this->title) == '') {
$this->setError('Title (title) is not set.');
return false;
}
if (trim($this->alias) == '') {
$this->alias = $this->title;
}
$this->alias = ApplicationHelper::stringURLSave($this->alias, $this->language);
// Ensure any new items have compulsory fields set
if (!$this->id) {
// Hits must be zero on a new item
$this-hits = 0;
}
// Set publish_up to null if not set
if (!$this->publish_up) {
$this->publish_up = null;
}
// Set publish_down to null if not set
if (!$this->publish_down) {
$this->publish_down = null;
}
// Check the publish down date is not earlier than publish up.
if (!is_null($this->publish_up) &&
!is_null($this->publish_down) &&
$this->publish_down < $this->publish_up) {
// swap the dates
$temp = $this->publish_up;
$this->publish_up = $this->publish_down;
$this->publish_down = $temp;
}
return true;
}
4. store()
The store()
sets created and modified dates, created by and modified by users, and also
checks for unique alias.
public function store($updateNulls = true)
{
$app = Factory::getApplication();
$date = Factory::getDate()->toSql();
$user = Factory::getUser();
if (!this->created) {
$this->created = $date;
}
if (!this->created_by) {
$this->created_by = $user->get('id');
}
if ($this->id) {
// Existing item
$this->modified_by = $user->get('id');
$this->modified = $date;
} else {
// Set modified to created date if not set
if (!$this->modified)) {
$this->modified = $this->created;
}
// Set modified_by to created_by user if not set
if (empty($this->modified_by)) {
$this->modified_by = $this->created_by;
}
}
// Verify that the alias is unique
$table = $app->bootComponent('com_depot')->getMVCFactory()
->createTable('Part','Administrator');
if ($table->load(['alias' => $this->alias]) &&
($table->id != $this->id || $this->id == 0)) {
$this->setError('Alias is not unique.');
if ($table->state == -2) {
$this->setError('Alias is not unique. The item is in trash.');
}
return false;
}
return parent::store($updateNulls);
}