rekowski.info David Rekowski's random stuff

##Typo3 plugin tutorial with extbase and fluid

2012-08-29

This tutorial describes how to create a simple Typo3 extension allowing to query a webservice for information.

Keywords

Typo3, Flow3, extbase, Fluid, extension, plugin, beginner, tutorial, how-to

Disclaimer

I am in no way fluent in Typo3, so I might get something conceptually wrong. Corrections are welcome.

Based upon the following article: Praxis-Workshop für Einsteiger: Extension-Entwicklung mit Extbase und Fluid.

1. Setup

I will assume you start with a fresh installation of Typo3. I used the introduction package of Typo3 4.7.4 (I like the theme color picker of the installer). The most common error I encountered is too restrict folder permissions, chmod -R 777 usually fixes this and is fine for a dev machine.

Make sure the following extensions are installed and enabled. They should be listed in the Extension Manager -> available extensions; use the filter.

  • extbase
  • fluid

If they are not, go to Extension Manager -> Import Extensions. Hit the ""database with green arrow" "Retrieve/Update" Button in order to load the extension list from the Typo3 repository. Then, search for extbase in the filter input field (hit enter) and install it. Afterwards, enable it in the "Installed Extensions" section (after installation you're automatically pointed there). Repeat for extension flow.

2. Getting Started

In your Typo3 installation, create a folder below typo3conf/ext with your extension name (I heard underscores are discouraged), we will use webgrep for this tutorial.

Create the following files and folder structure:

webgrep
  |- Classes
  | `- Controller
  |   `- AppController.php
  |- Resources
  | `- Private
  |   `- Templates
  |     `- App
  |       `- index.html
  |- ext_emconf.php
  |- ext_localconf.php
  `- ext_tables.php

3. Step A: The plugin frame

This step will result in a static output from the plugin. This may be already what you need as a starting point.

ext_emconf.php

We will start with ext_emconf.php. This is basically the registration information for your plugin:

<?php

$EM_CONF[$_EXTKEY] = array(
    'title' => 'Webgrep',
    'description' => 'Fetch and display remote data',
    'category' => 'plugin',
    'author' => 'David Rekowski',
    'author_company' => '',
    'author_email' => 'mail@rekowski.info',
    'dependencies' => 'extbase,fluid',
    'clearCacheOnLoad' => 1,
    'version' => '0.0.1',
    'constraints' => array(
        'depends' => array(
            'php' => '5.3.0-0.0.0',
            'typo3' => '4.7.4-0.0.0',
            'extbase' => '0.0.0-0.0.0',
            'fluid' => '0.0.0-0.0.0',
        ),
    )
);

Most portions may be obvious. Note the constraints, though, which allows you to define, which versions of php, typo3 or any plugins are required. You can set an upper boundary, I assume 0.0.0 means no limit upwards.

ext_localconf.php

Next up ext_localconf.php. You probably see more condensed versions out there, I prefer verbose formatting. This code section apparently configures the plugin in some way or another.

<?php
if (!defined ('TYPO3_MODE')) {
    die ('Access denied.');
}

Tx_Extbase_Utility_Extension::configurePlugin(
    $_EXTKEY,
    'Pi1',
    array(
        'App' => 'index'
    ),
    array(
        'App' => 'index'
    )
);

The method signature specifies:

configurePlugin(
    $extensionName, 
    $pluginName, 
    array $controllerActions, 
    array $nonCacheableControllerActions = array(), 
    $pluginType = self::PLUGIN_TYPE_PLUGIN
)

I guess Pi1 is only used internally, so the name does not seem to matter. I don't know anything about caching with Typo3, so I just define the index controller action to be not cachable. The App key signifies the controller folder name and class name portion, we'll get there in a minute. You may specify multiple, comma-separated controllers, e.g. 'index,add,create', but we currently only need one. As far as I know, if a link specifies no action, the first one is used. Doesn't concern us right now.

ext_tables.php

Regarding ext_tables.php, the name is (like so many I stumbled over in Typo3, let alone lots of abbreviations) not quite intuitive. You can specify a domain model here, but since we don't need it, this is just where the plugin is registered.

<?php
if (!defined ('TYPO3_MODE')) {
    die ('Access denied.');
}

Tx_Extbase_Utility_Extension::registerPlugin(
    $_EXTKEY,
    'Pi1',
    'Webgrep'
);

AppController.php

Remains the AppController.php. This is the actual hook for your application. You specify a controller action (naming convention <action>Action).

<?php

class Tx_Webgrep_Controller_AppController
    extends Tx_Extbase_MVC_Controller_ActionController {

    public function initializeAction() {
    }

    /**
     * @return string The rendered view
     */
    public function indexAction() {
        $this->view->assign('result', array('test' => 'blupp'));
    }
}

The initializeAction method is called regardless of the action type. The appropriate action is executed.

All we do is assign an array to the view variable result. We can then access it in the template.

index.html

The index.htmlis a Fluid template file. You can access assigned variables (may be a value, array or object) by enclosing it with curly brackets, e.g. {result.test}. You see, you can access object properties or array values using the dot-operator, <array-or-object>.<key-or-property>; may be recursive for nested objects and multidimensional arrays. Thus our template looks mighty impressive:

<div>
    {result.test}
</div>

And that's basically it for a simple Typo3 module. If you are not interested in integrating a webservice and only need a starting point for your own module, you can skip to section Install extension. Stay tuned though, for a take on querying twitter for posts tagged with #typo3.

4. Step B: Implementing the grabbing part

This step illustrates, how the previously constructed plugin scaffold can be extended to integrate some application logic, in this case fetching some website content and parsing it.

All we have to do is alter the AppController.php and the index.html Template. Actually, you would want to put the application logic into a separate class, but we will refrain from doing so for simplicity's sake.

So our new AppController.php looks like this:

<?php

class Tx_Webgrep_Controller_AppController
    extends Tx_Extbase_MVC_Controller_ActionController {

    const WEBGREP_URL = 'https://mobile.twitter.com/search?q=%23typo3';
    const XPATH_TWEETS = ".//table[@class = 'tweet']";
    const XPATH_USER = ".//span[@class = 'username']";
    const XPATH_TEXT = ".//div[@class = 'tweet-text']";

    public function initializeAction() {
    }

    /**
     * @return string The rendered view
     */
    public function indexAction() {
        $result = $this->query(self::WEBGREP_URL);
        $this->view->assign('result', $result);
    }

    private function query($url) {
        $content = file_get_contents($url);
        $result = $this->parse($content);
        return $result;
    }

    private function parse($content) {
        $result = array();

        $doc = new DOMDocument();
        $doc->loadHTML($content);
        $xpath = new DOMXPath($doc);

        $tweets = $xpath->evaluate(self::XPATH_TWEETS);
        for ($i = 0; $i < $tweets->length; $i++) {
            $tweet = $tweets->item($i);
            $nameNodes = $xpath->evaluate(self::XPATH_USER, $tweet);
            $name = $nameNodes->item(0)->textContent;
            $text = $xpath->evaluate(self::XPATH_TEXT, $tweet);
            $result[] = array(
                'user' => trim($name),
                'text' => $text->item(0)->textContent
            );
        }
        return $result;
    }
}

Instead of assigning a static value to the result view property, I pass the result of query(). The query() method retrieves the content from the given URL and passes it to parse(). In parse() the content is read as HTML into a DOMDocument and parsed using XPath. It would lead us too far to enter into details, suffice it to say, that we iterate over all tweet nodes and query for the username and the tweet text. The Tweet text may contain HTML, which is stripped by using only the textContent. The template variable result now contains an array with a list of tweets, each of which is an array with the keys user and text.

Thus, the template index.html needs to be altered to looks like this:

<div>
    <f:for each="{result}" as="tweet">
        <p>
            <strong>
                <a href="https://twitter.com/{tweet.user}">{tweet.user}</a>
            </strong>:
            {tweet.text}
        </p>
    </f:for>
</div>

We iterate over the result variable and output a paragraph with a linked username and the tweet text. That's it.

5. Install the extension

The extension should now be listed in the Extension manager. You can install it by clicking on the brick+plus icon left to the extension name.

6. Embed plugin in page content

Create a new page of type Page > Standard and enter a page title. I read you have to specify

page = PAGE
page.10 < styles.content.get

in your Resources -> Page TSConfig configuration, but it appears it works without.

Close the page configuration and add a content section to the Normal column. Select Plugins -> General Plugin. Optionally define a header. In section Plugins, select the Webgrep plugin from the dropdown. Change to the view mode (left menu) and note the output.

Caveats

  • If anything went wrong with your plugin, you may have to clear the cache via the lightning icon in the top right area.
  • If your plugin doesn't appear to work, try to create a new page and add the plugin to the content.

Notes

  • Yeah, I didn't bother with unit tests for simplicity's sake.
  • There ought to be a Typo3 autoloader, if you use separate classes, have a look at that.
  • The code is not PSR-X compatible, duh.