Command disabled: revisions

Programming Extensions

General architecture

It is perfectly possible to write quite an amount of very bad code in just one extension. How to prevent this?

  • Keep as much of the data retrieval on the database side. Let the database server handle things like sorting, grouping, etc..
  • Separate style/layout and programming logic. Use template files.
  • Be smart about data binding. Use the default Typo3 methods for this.

I have run into some trouble with regard to the first point though, because the MySQL server on my system does not seem to support much more than the most basic SQL syntax. EXIST clauses invariably result in parse errors, no matter how much I refrain from aliasing tables (another problematic issue).

Debugging

Use the wonderfully documented writelog() function to log all errors or messages to the table sys_log. Go to the Tools module and select Log to view the sys_log table (so: “Tools > Log”).

There is an interface function writeLog($action,$error,$details_nr,$details,$data) in class.t3lib_extfilefunc.php, but the real definition is in class.t3lib_userauthgroup.php:

function writelog ($type, $action, $error, $details_nr, $details, $data, $tablename='', $recuid='', $recpid='', $event_pid=-1, $NEWid='')

One warning though: writelog does not seem to work in frontend extensions. And there really is hardly any documentation.

Alternatively, use the static t3lib_div::debug($this→piVars) function to output environment variables (GET and POST), or any other array (strings do not seem to work).

File operations

You could use class.t3lib_extfilefunc.php. This works smoothly, but not under Windows:

if ($this->actionPerms['deleteFolderRecursively'])	{
	// No way to do this under windows
	$cmd = 'rm -Rf "'.$theFile.'"';
	exec($cmd);		// This is a quite critical command...

As you can see, a unix specific command is issued here…

Building Forms

Remember to:

  1. specify:
$this->pi_USER_INT_obj=1;
  1. and also to include:
<input type="hidden" name="no_cache" value="1"/>

Modifying System Extensions

To modify most of the system extensions, you need to do some hacking. A lot of system extensions seem to be based on libraries of general functions, found here: /typo3/sysext/cms/tslib/.

For instance, to modify the standard mail form, you have to hack the /typo3/sysext/cms/tslib/class.tslib_content.php file. Here you'll find a function FORM, which determines how the html code for the form is shaped. However, any changes you make in this function will affect all forms, not just the standard mail form!

TypoScript for your Extension

Remember: all typoscript goes into setup and constants files - you cannot put any custom made typoscript into Page TSConfig! See Building Flexforms for a way to configure your extension on each page where it is included.

You can, however, use typoscript in the constants and setup sections of your template, once you've made your setup files for the extension.

Note added 20050526: there seems to be a problem with adding your own plugin constants in the constants section of the TS-template (problem is in the backend, probably not in setting constants through plugin TS-files). So, just put your sitewide extension variables in the setup part of TS-templates…

To summarize this:

  • Side-wide typoscript goes into the setup section of your template;
  • Page specific typoscript goes into the flexforms.

JavaScript for your Extension

The class tslib_fe (fe = frontend) contains various methods to configure the frontend side. One of them is:

/** 
 * Sets JavaScript code in the additionalJavaScript array
 *
 * $key is the key in the array, for num-key let the value be empty
 * $content is the content if you want any
 * Note reserved keys "openPic" and "mouseOver"
 */
function setJS($key,$content="")	

Class tslib_fe is instantiated as $GLOBALS[“TSFE”], so adding some JavaScript to your extension involves calling the method in your extension class, which is typically located in:

/typo3conf/ext/myExtensionKey/pi1/class.tx_myExtensionKey_pi1.php)

The call could look like this:

$GLOBALS["TSFE"]->setJS('myExtensionKey','alert("hello!");');		

Here, the javascript snippet alert(“hello!”); is inserted directly in the <head> section of any page containing your plugin. The benefit: you exert php-side control over the javascript (usefull for adding translations).

Another method (one without the benefit of php-control over javascript) is including javascript files in the <head> section, e.g.:

$GLOBALS['TSFE']->additionalHeaderData[$this->extKey] = '<script type = "text/javascript" src = "'.t3lib_extMgm::siteRelPath($this->extKey).'featurematrix.js"></script>';

You can safely do this in the context of your plugin class, where $this→extKey is defined as the key of your extension.

The property additionalHeaderData is an array where you can add your own content (any content, not just javascript) for the <head> section of the html page.

The static class t3lib_extMgm has a method siteRelPath which returns the path to your extension as seen from the website.

Keep String in one Piece with Character Encoding

This is really a general JavaScript issue - but you're bound to run into it when using Typo3, so here goes…

Problem: in some browsers, a javascript string within double quotes containing a single quote (“such as this one, where I've inserted a single quote”), will turn sour. E.g., IE considers the single quote a string terminator, even though the string was really started with a double quote.

Solution: use the PHP rawUrlEncode function to encode strings. Then decode them in Javascript using the JavaScript function unescape.

Building Flexforms

Use Flexforms to configure your plugin on a page-by-page basis, in the backend. There are some pitfalls here.

All Flexforms elements are based on the TCA (global array) field types. These field types are defined in arrays.

Radio button array structure not exactly mapped to XML

This is an example of a radio button definition for a given table.

    'base' => Array (
            'label' => 'BASE',
            'config' => Array (
                'type' => 'radio',
                'items' => Array (
                    Array('absolute (root) / ', 0),
                    Array('relative ../fileadmin/', 1)
                ),
                'default' => 0
            )
        )

We would expect this to translate to an xml structure where an element <default> has <config> as its parent element. This is right now (20050201) not the case however. The correct xml is:

<config>
   <items>
   ...
      <default>...</default>
   </items>
</config>

In other words: the correct location for <default> is as a child element of <items> instead of <config>.

Making a flexform for your extension

  • You need an xml file which defines the flexform. Example: flexform_ds.xml.
  • Add the following lines to the file ext_tables.php:
$TCA['tt_content']['types']['list']['subtypes_addlist'][$_EXTKEY.'_pi1']='pi_flexform';
t3lib_extMgm::addPiFlexFormValue($_EXTKEY.'_pi1', 'FILE:EXT:so_projectgroups/flexform_ds.xml');

Replace the highlighted extension key with your own. Also, be sure that the xml file referenced in the same line matches the name of your xml file.

  • The fields in your flexform must be translated in the file locallang_tca.php. Example:
<?php
 
$LOCAL_LANG = Array (
	'default' => Array (
		'so_projectgroups' => ' Configuration Project Groups ',
		'so_projectgroups.entryPoint' => ' Entry point for Feature Matrix: ',
		'so_projectgroups.entryPoint.startOnProducts' => ' show product list ',
		'so_projectgroups.entryPoint.startOnCriteria' => ' show criteria/features list ',
	),
		"nl" => Array (
	)
);	
?>

This should be enough to get you started (plus maybe emptying your Typo3 cache).

Hacking the backend forms through $TCA

First things first: what does TCA stand for? Nobody seems to know or care, so I have made up my own words: Typo3 Configuration Array. Sounds plausible doesn't it?

This is what is does: it holds the configuration for backend forms (well, among others). $TCA specifies such things as the size of a particular select box, or the number of columns in some other form element. It is also a mechanism for data binding: the $TCA knows exactly, for instance, which record field in your extension should be fed to what form element, and what table the record comes from. It knows this, because you configured the $TCA array for your extension, by using the kickstarter wizard!

Besides the configuration for you extension, there is also a “central” $TCA, which is accessible throughout the whole system. Use it in your own functions by declaring $TCA as a global variable:

global $TCA;

So, each time you use the kickstarter wizard, you also extend the $TCA. Not directly, because all TCA con­figurations for all extensions would add up quickly and result in a very fat t3lib/stddb/tables.php file indeed (this is where the main $TCA resides). Instead, each extension con­figuration is gathered in a file called tca.php. Aha, that is what this mysterious file in your extension's directory is for!

Configuration data pertaining to a particular extension is not automatically available. If, for instance, you would like to use a form based on a table from some other extension, you would have to load the $TCA data for this table explicitly. You do this by calling a special function:

t3lib_div::loadTCA($tableName);

Use another method for the frontend:

tslib_fe::includeTCA();

This latter method sets the entire $TCA. Your attention is required here, however. For it seems that includeTCA() does not load the column configuration for the tables. So, if you use includeTCA() and you need the column configuration out of the $TCA for a particular table, always check if the columns array is set. E.g.:

tslib_fe::includeTCA();
if (!is_array($TCA[$table]['columns'])) {
   t3lib_div::loadTCA($table);
}

(The :: operator means that you call a method on a static, non-instantiated class. In other words: you do not have to use the new operator for the t3lib_div class.)

The backend forms are also known as tceforms, and they are rendered in the backend by specialized functions which reside in t3lib/class.t3lib_tceforms.php.

For more information on the structure and the semantics of the $TCA array, see the TYPO3 Core APIs documentation (extension key: doc_core_api).

Tweaking the order of option values in combo boxes (select boxes)

The kickstarter wizard does something odd with regard to combo boxes (as I prefer to call single select boxes). You can specify that a the values for a combo box (the option keys and values) should be derived from a foreign table. So far so good. But if you look in the backend, and you add a new record, you will notice that the contents of the combobox do not appear very orderly. In fact, they are ordered by uid, instead of label.

So, go to the tca.php file in your extension's directory, and look for the column (called a field in $TCA) which contains the value you select with the combobox. This will normally be a foreign key. The information regarding the foreign table is located here:

$TCA[$yourTableName]['columns'][$yourColumnName]['config']['foreign_table']

On the same level as ['foreign_table'] you will also find the where clause [' foreign_table_where'] used to retrieve the information from the foreign table. The $TCA configuration could look like this:

"foreign_table_where" => " ORDER BY tx_jullecompanydirectory_company.uid "

As you can see, the records from the foreign table are ordered by uid. Change this by simply using valid SQL. For instance:

"foreign_table_where" => " ORDER BY tx_jullecompanydirectory_company.company_name "

Now, the combo box values will be ordered alphabetically, assuming of course that the $TCA for the foreign table has the same column used as a label, as the one you specified for ordering. Find this out by looking at: $TCA[yourForeignTableName]['ctrl']['label'].

Using our previous example, this could look like:

$TCA['tx_jullecompanydirectory_company']['ctrl']['label']

Adding a new field to your extension without the kickstarter

Warning: modifying your extension without the kickstarter will make future use of the kickstarter impossible for this particular extension. Why is this? Well, the kickstarter adds two files, wizard_form.dat and wizard_form.html, which are just impossible to modify by hand. If you don't believe me, go ahead and take a look in the /doc directory of your extension, where you'll find the aforementioned files.


So, once you've added a new field to your extension without using the kickstarter, the situation in the kickstarter will no longer correctly reflect your extension's configuration.


To add a new form field to your extension, you need to modify four files:

tca.php

In the columns array, add your new field. Take a look at the other fields (i.e. columns) listed there to get a feeling for the structure of this array. Pay attention to the label entry for your column: fill this entry with a translation key.

At the end of the tca-table definition, add your new column name in the types array.

locallang_db.php

This is the file containing all field label translations. Add your own translation, using the translation key from tca.php.

ext_tables.sql

Add your column to the table, using standard SQL.

ext_tables.php

This is another configuration file for the global $TCA variable. Inside the “feInterface” array, you'll find an entry “fe_admin_fieldList”. List your own column here as well.

You should now be able to see your own form field in the backend form for your extension.

Related comboboxes a.k.a. dependent selectboxes

A selectbox can be related to (or, dependent on) another selectbox. For example, if you add comboboxes for organizations and departments in a backend form, it would be nice to see only the departments for the organization you have just selected.

If left alone, Typo3 will happily show you all departments in the entire table, no matter which organization you have just chosen.

To remedy this situation, you must make sure that the departments combobox is filled only with departments which belong to the organization as chosen in that particular selectbox.

There are two ways to achieve this result:

If you use an advanced database server

For MySQL 4.1 or higher (or any other advanced database server), you can use subqueries to restrict the set of retrieved departments. In this example, we assume that this is the TCA configuration for the table fe_users.

  • In the $tca array (found in \typo3conf\ext\your_extension\ext_tables.php or \typo3conf\ext\your_extension\tca.php), find the column which holds the foreign keys for the departments (just to follow through our example);
  • Now, look for the foreign_table_where key under this column. Add a subquery:
department = (SELECT departments.uid FROM departments, fe_users WHERE fe_users.uid=###THIS_UID### AND fe_users.organization = departments.parentorganization)

Here, the relation is established through the use of ###THIS_UID###, which is the current element uid (zero if new). In other words: the primary key for the current record out of the the table fe_users (the proper context would be a form containing fe_users information for a single user, such as the organization and the department to which the user belongs).

Warning: MySQL versions prior to 4.1 do not support subqueries! Typo3 does not assume any particular MySQL version, so it is not wise to use this solution if you plan to make your extension public.

Normally you would rewrite the subquery mentioned above to something like:

... FROM departments, fe_users WHERE fe_users.uid=###THIS_UID### AND fe_users.organization = departments.parentorganization

But within the context of the TCA this is impossible: you can only specify one foreign table, no joins.

If you want to keep maximum database compatibility

So, for maximum database compatibility, choose another solution: call a user defined function from inside the TCA. This function fills the array which holds the items for the combobox. This array is automatically passed by reference to your function, along with the parent object (t3lib_TCEforms / t3lib_transferData depending on context). Now, what do you need to do?

  • Make an extension which holds your function, or use an existing one and create a new class.
  • Be sure to include the class which holds your function into your backend extension. Add the following line to your backend's ext_localconf.php file:
require_once (t3lib_extMgm::extPath('so_feuserorgdep'). 'class.tx_sofeuserorgdep_helper.php');
  • Have the function populate the selectbox array with the right items:
function getDepartments(&$params,&$pObj) {
		global $TYPO3_DB;
 
		$userOrganization = (!isset($params['row']['tx_sofeuserorgdep_organization']) || ($params['row']['tx_sofeuserorgdep_organization'] == 0) ) ? -1 : $params['row']['tx_sofeuserorgdep_organization'];
		$fields = 'uid,departmentname';
		$from = 'tx_sofeuserorgdep_departments';
		$where = 'parentorganization = '.$userOrganization.' AND hidden=0 AND deleted=0 ';
 
		$res = $TYPO3_DB->exec_SELECTquery ($fields,$from,$where,'','');				
		while($row=$TYPO3_DB->sql_fetch_assoc($res))	{			
			$params['items'][]=Array($row['departmentname'],$row['uid']);
		}		
	}	
}

As you can see, the array which is automatically passed to your function not only contains a placeholder for the items, but also TCA-derived information concerning the current record, such as the a foreign key for 'organization'.

To use this function, follow these steps:

  • In the $tca array (found in \typo3conf\ext\your_extension\ext_tables.php or \typo3conf\ext\your_extension\tca.php), find the 'config' key for the column which holds the foreign keys for the departments (again, we use the organizations and departments example);
  • Under this key, add the following line:
"itemsProcFunc" => 'tx_sofeuserorgdep_helper->getDepartments'

Now, whenever the backend needs to fill an fe_user's combobox with departments, our own function is called. This function retrieves all departments for us which belong to a previously selected organization.

The only drawback (with both solutions) is that you must first save the record after you have selected an organization, before the right set of departments is loaded.

Build Frontend forms real fast: use the kickstarter wizard

Have you ever gotten the feeling that you were doing the exact same thing twice? First you build the backend forms using the kickstarter wizard, then you do it all over again to expose data from the same table to the frontend.

Well, Robert Lemke has build a nifty library to produce frontend table-based forms pronto. Simply install the extension frontendformslib, and take a look at the examples (and the documentation, of course).

With frontendformslib all it takes to produce frontend forms, is for you to name table and some columns. The library then delves into the $TCA to find out what you have specified earlier on for the display of the exact same table in the backend. In other words: the $TCA is no longer exclusively used in the backend, but also in the frontend.

The extension frontendformslib could be considered a minimalist version of the t3lib_tceforms class.

How to output just data (e.g. xml), omitting layout

Your Flash movie needs an xml stream? Or your Java applet needs serialized data? Or, generally speaking, your plugin should work in two modi, serving up ordinary webpages or data?

Then use an extension template which reacts to a page type. We are assuming here that you have already set up the normal situation: your plugin serves up ordinary html pages, using the default template for the website. Now for the data output. Say, you want to output an xml file.

Go to the page where your plugin resides. We are going to configure the page in such a way that it reacts to special calls, made through the query string.

First, make a new extension template for this page. Within the setup field, set the typeNum property of the page to an arbitrary number, for instance typeNum = 28234, but not 0 (or any other number already taken by e.g. a framed page). This typeNum property is crucial: you use it in your querystring to call for the data output instead of ordinary output (which would be called by typeNum = 0).

If you now call this page by using the special type, the extension template is activated:

http://maakzelf/index.php?id=153&type=28234

So, what should the template setup look like? Well, you will probably want to call a special function within your extension, which handles this situation. Here is an example:

so_miniwordshooter >
so_miniwordshooter = PAGE
so_miniwordshooter {
  typeNum=28834
  config.disableAllHeaderCode = 1
  config.additionalHeaders = Content-type:text/xml
includeLibs.so_miniwordshooter = EXT:so_miniwordshooter/pi1/class.tx_sominiwordshooter_pi1.php
  10 = USER
  10 {
    userFunc = tx_sominiwordshooter_pi1->getGameData
  }
}

In this example, a user defined function (i.e. a php function) is called. Presumably, it returns xml, since we have also set the config property of the page to Content-type:text/xml. Also, notice that the value for the typeNum property correspondes to the value of the query parameter type. This, in and by itself, is not enough to have this template called, however: your template must really be sitting on the page you are calling (in this example the page with the id “153”).

Outputting ordinary content as x(ht)ml

You can also choose to output your ordinary page content as x(ht)ml. To output plain xml without the intervention of user functions or html templates, use the following typoscript in an extension template (i.e. a typoscript template).

# template example to put in your template setup
tt_content.stdWrap.dataWrap >
xml_contentrendering >
xml_contentrendering = PAGE
xml_contentrendering {
	typeNum=0
	config.disableAllHeaderCode = 1
	config.additionalHeaders = Content-type:text/xml
	config.admPanel = 0
	config.xhtml_cleaning = 0
	10 = TEXT
	10.value = <?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
	20 = CONTENT
	20 < styles.content.get
}

(Example taken from http://typo3.org/fileadmin/pdf_manuals/manual-intera_photo_xml-16-02-2005_10-14-17.pdf)

Notice that xml_contentrendering is just a variable name: it's not a typoscript object.

Getting a Flash movie to work in MS IE

Okay, this is not Typo3-related, but I had some problems with it which at first I attributed to Typo3 (yeah, go ahead, slap me in the face…), when I was trying to figure out how to send xml to a Flash movie. It worked allright in the other browsers (Firefox, Opera), so it was soon clear that it was an MS IE issue…

To embed a Flash movie in your web page, use the <object> tag. Within the 'object', you can use parameters (<param> tags) which take a name and a value attribute. There is also an <embed> tag, where you state the source (using the 'src' attribute), just as you would for an image. State the whole path here, including any query string parameters you may have. Use a param tag to specify the movie file.

Example:

<object classid="clsid:some_id" ... >
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="wordshooter.swf" />
<param name="quality" value="high" />
<param name="bgcolor" value="#52aaf5" />
<embed src="/games/wordshooter.swf?id=10&my_qparam=11" ..  />
</object>

This is also the kind of (x)html code that the Macromedia Flash-editor produces.

But wait! MS Internet Explorer has some problem with this. To get this working in MS IE, you must also state the full path (and any query string parameters you may have) in <param name=“movie” value=”…”/>.

MS IE compatible example:

<object classid="clsid:some_id" ... >
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="/games/wordshooter.swf?id=10&my_qparam=11" />
<param name="quality" value="high" />
<param name="bgcolor" value="#52aaf5" />
<embed src="/games/wordshooter.swf?id=10&my_qparam=11" ..  />
</object>

In other words: repeat the contents of the 'src' attribute in the “movie” param tag.

Save your Flash movie's freshness: Http header: Cache-Control

MS IE 6 is champion in letting your Flash movies get stale. Somehow, the Flash file will not go back to the server to retrieve dynamically generated data. The solution is to explicitly instruct the server to send out a specific http header:

"Cache-Control: no-cache, must-revalidate"

In php, you just use the header() function:

header("Cache-Control: no-cache, must-revalidate");

This header must be output before anything else, on the page where your Flash movie is embedded.


Personal Tools