Quick Start – What a Weblog looks like with Tekuna

This document gives you an overview of Tekuna's principles, architecture and features. For demonstration a simple weblog application is developed. You can download this demo application on the downloads page.

Before you can start working with Tekuna you have to download and unzip it within your webserver's document root. Make sure that the PHP version is 5.2.1 or higher. A database is not needed for this example, that stores its data in text files.

Contents

  1. Defining the application's structure
  2. Defining the basic layout
  3. Creating Controller, Model and View
  4. Matching URL parts, handling exceptions and using translations
  5. Generating other output types
  6. The MVCController workflow and input data filtering
  7. Conclusion

Defining the application's structure

Planning a Tekuna based application is thinking about pages, components and URLs accessing the pages. These things are defined in a central application.xml. Every Tekuna application is separated into several modules. Each module has its own layout template and components that fit into the layout. A simple weblog could be consisting of two modules. One for serving all the HTML pages and another module for feeds.

<?xml version="1.0" encoding="UTF-8" ?>
<application languages="en,de">

	<module baseURL="/feeds/" layout="feed_atom.tpl">
		<!-- ... -->
	</module>
	
	<module baseURL="/" layout="default.tpl">
		<!-- ... -->
	</module>

</application>

This (incomplete) file defines an application with two modules. The application will be supporting the languages english and german. The default module will be accessible at the root URL and the feed module at the base URL "/feeds/". The default module uses a standard XHTML template whereas the feeds will be rendered in an ATOM XML template.

Each of these referenced layout templates contains a bunch of so-called slots. These are place holders, where content is filled in. To every slot is defined a component. The output of the component can be either static (this is called a fragment) or dynamic (which references a controller). If a slot should contain different contents depending on the request URL, routes define controllers or fragments for URL pattersn.

<?xml version="1.0" encoding="UTF-8" ?>
<application languages="en,de">

	<module baseURL="/feeds/" layout="feed_atom.tpl">
		<component slot="data">
			<route pattern="articles.xml" controller="ArticlesFeed" />
		</component>
	</module>
	
	<module baseURL="/" layout="default.tpl">
		<component slot="header" fragment="header.htmlf" />

		<component slot="content">
			<route pattern="" controller="LatestArticles" />
			<route pattern="index.html" controller="LatestArticles" />
			<route pattern="article/*.html" controller="Article" />
			<route pattern="add_article.html" controller="AddArticle" />
			<route pattern="**" fragment="404.htmlf" status="404" />
		</component>

		<component slot="footer" fragment="footer.htmlf" />
	</module>

</application>

The layout of the standard module of our weblog contains three slots: header, content and footer. Header and footer are filled with static content from fragments whereas the content slot's content depends on the URL. If the page is called directly or the page index.html is called, the LatestArticle controller is used. If the URL starts with article/ and ends with .html the Article controller generates the content. If the URL add_article.html is requested, the AddArticle controller is used. If none of these rules matches the request URL, the last route is checked. This route matches all possible URLs on any directory depth and serves a static 404 fragment. Additionally the response status is set to 404, too.

For the feeds module only one component is defined. The only valid URL is "/feeds/articles.xml" which triggers the ArticlesFeed controller that generates an ATOM feed for the existing articles and fills the contents into the feed_atom.tpl template. So the simple weblog application's structure is defined completely now.

Defining the basic layout

The next step is to create the referenced layout templates and static fragments. The further explanations are made only for the standard module. The development of the feeds module is very similar. The layout template default.tpl could look like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
                       
<html xmlns="http://www.w3.org/1999/xhtml" 
      lang="{context.applicationLanguage}" 
      xml:lang="{context.applicationLanguage}">
	     
	<head> 
		<title>Tekuna Demo Application</title> 
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
		<meta name="language" content="{context.applicationLanguage}" />

		<link rel="stylesheet" type="text/css" 
		      href="{context.relativeBasePath}styles/screen.css"
		      media="screen,projection" />
	</head> 
	<body>
		<div id="wrapper">
			<div id="header">
				{header}
			</div>
	
			<div id="content">
				{content}
			</div>
	
			<div id="footer">
				{footer}
			</div>
		</div>
	</body> 
</html>

This is a standard XHTML 1.0 template that defines the three slots for header, content and footer. Furthermore there are some other place holders: context.applicationLanguage and context.relativeBasePath. These are filled automatically with the corresponding values from the application context. The applicationLanguage contains the currently active language (in our case "en" or "de", as defined in the application.xml configuration file). The relativeBasePath contains a relative path to the application root. All relative links must be prepended with this place holder to enable correct navigation and loading of referenced resources (such as CSS and JavaScript).

Now it's time to access our application. Just point your browser to the application's root directory. After you hit enter, the small application shows up with three exceptions like this one:

The fragment for slot 'header' could not be loaded. The fragment 'fragments/en/header.htmlf' does not exist or is not readable.

This exception tells you quite well what's happened. To avoid this message just create the needed header-fragment in the file "fragments/en/header.htmlf". At the path you can see that fragments are loaded language-dependent. So it is easily possible to use another header text for the german and the english version of the weblog. After creating the header, footer and 404 document fragments for both languages and some lines of CSS in the styles/screen.css file our weblog looks like this:

The controller for slot 'content' could not be executed. Controller class file not found.

The remaining exception says that there is still the controller missing, thats output should fill the content slot. Note that there is no critical information displayed with this exception. The exception trace was logged to the file log/exceptions.log. There you can see the following information:

2009-08-08 16:08:57	TekunaApplicationException
Message: The controller for slot 'content' could not be executed. Controller class file not found.

File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
Line: 176
Trace:
	#0	DefaultModuleProcessor -> processController (ApplicationComponent Object, 'LatestArticles')
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 236

	#1	DefaultModuleProcessor -> processRoutes (ApplicationComponent Object)
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 86

	#2	DefaultModuleProcessor -> processModule (ApplicationModule Object)
			File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
			Line: 164

	#3	TekunaApplication -> handleRequest ('/')
			File: /demo/index.php
			Line: 36

	#4	{main}


Caused by: TekunaApplicationException
Message: Controller class file not found.

File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
Line: 241
Trace:
	#0	TekunaApplication :: getControllerInstance ('LatestArticles', TekunaApplicationContext Object)
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 168

	#1	DefaultModuleProcessor -> processController (ApplicationComponent Object, 'LatestArticles')
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 236

	#2	DefaultModuleProcessor -> processRoutes (ApplicationComponent Object)
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 86

	#3	DefaultModuleProcessor -> processModule (ApplicationModule Object)
			File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
			Line: 164

	#4	TekunaApplication -> handleRequest ('/')
			File: /demo/index.php
			Line: 36

	#5	{main}


Caused by: TekunaClassLoadException
Message: The class or interface 'components.controller.LatestArticles' cloud not be loaded.

File: /demo/_framework/org/tekuna/base/TekunaClassLoader.class.php
Line: 102
Trace:
	#0	TekunaClassLoader -> loadClass ('components.controller.LatestArticles')
			File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
			Line: 205

	#1	TekunaApplication :: load ('components.controller.LatestArticles')
			File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
			Line: 237

	#2	TekunaApplication :: getControllerInstance ('LatestArticles', TekunaApplicationContext Object)
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 168

	#3	DefaultModuleProcessor -> processController (ApplicationComponent Object, 'LatestArticles')
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 236

	#4	DefaultModuleProcessor -> processRoutes (ApplicationComponent Object)
			File: /demo/_framework/org/tekuna/core/application/DefaultModuleProcessor.class.php
			Line: 86

	#5	DefaultModuleProcessor -> processModule (ApplicationModule Object)
			File: /demo/_framework/org/tekuna/core/application/TekunaApplication.class.php
			Line: 164

	#6	TekunaApplication -> handleRequest ('/')
			File: /demo/index.php
			Line: 36

	#7	{main}

Every Tekuna exception can have a causing exception. So if you catch an exception and throw another one, the root cause does not get lost and is logged, too. Apart from that the traces are clearly represented and easily readable.

Creating Controller, Model and View

Now it's time to create the LatestArticles controller. The PHP file must be located in components/controller/LatestArticles.class.php. Every controller must implement the TekunaController interface (API). This interface forces a default constructor to provide the current application context for the module and a simple getOutput() method.

You can implement this interface manually but it is more comfortable to derive own controllers either from the AbstractController (API) or the AbstractMVCController (API), that provides a MVC implementation. The corresponding model and view are loaded automatically, as well as a simple translation facility. To declare a simple MVC controller for the display of the latest articles write a class like this:

<?php

	TekunaApplication :: load('components.model.ArticleModel');

	class LatestArticles extends AbstractMVCController {
		
		protected
			$sViewClass = 'PHPView',
			$bTranslateOutput = false,
			$bAutoloadModel = false;
		
		public function display(Request $objRequest, TekunaView $objView) {

			// find all articles
			$arrArticles = ArticleModel :: findAll();
			
			// show articles in reverse order
			$objView -> assign('articles', array_reverse($arrArticles));
		}
	}

The protected class attributes set some properties for this controller. The used template engine is the simple PHPView (API), that uses just a plain PHP file with the assigned variables. Other built-in possibilities are SimpleView (API) and XSLTView (API). The usage of other template libraries like Smarty is also possible.Output translation is not used in this case and the default model class (which would be in this case a class named LatestArticlesModel) is not used. The only method to implement the display of some content is display(...). There all available articles are loaded from the model class ArticleModel and are directly assigned to the view. The very first statement of this file loads this ArticleModel class, that resides in the folder components/model/.

<?php

	Tekuna :: load('org.tekuna.core.persistence.TextDB');

	class ArticleModel implements TekunaModel {
		
		public
			$id = -1,
			$date = 0,
			$title = '',
			$text = '';
		
		
		private static function getDB() {
			
			return new TextDB('articles', array('date', 'title', 'text'));
		}
		
		
		public static function findAll() {
			
			$arrArticles = array();
			
			foreach (self :: getDB() -> selectAll() as $arrData) {
				
				$objArticle = new ArticleModel();
				$objArticle -> id = (int) $arrData['id'];
				$objArticle -> date = (int) $arrData['date'];
				$objArticle -> title = (string) $arrData['title'];
				$objArticle -> text = (string) $arrData['text'];
				
				$arrArticles[] = $objArticle;
			}
			
			return $arrArticles;
		}
	}

This simple model class uses the TextDB class, that provides a basic database with flat file storage. By using the Tekuna :: load(...) method this class is made available for the model class. The findAll() method selects all entries from the text-database-table and returns an array of ArticleModel objects. For productive systems it is not recommendet to use the TextDB. By using a real database and an ORM-framework, model classes can be written more easily. To provide some data for our small text database just paste the following lines into the file textdb/articles.textdb:

3
1	|||	1249746283	|||	My first Weblog Article	|||	<p>This is my very first weblog article.</p>
2	|||	1249748283	|||	My second Weblog Article	|||	<p>This is just the next try...</p>

The default view to display the content is loaded from components/view/ArticleModel.php. This file does some really simple output of the loaded articles.

<?php

	foreach ($articles as $article) {

		$title = $article -> title;
		$link = "{context.relativeBasePath}article/{$article -> id}.html";
		
		echo "<h1><a href=\"$link\">$title</a></h1>";
		echo $article -> text;
	}

For every found article the title and text are displayed. The title contains a link to the article's detail page. If you now access the page with your browser you will see a list of blog articles:

Matching URL parts, handling exceptions and using translations

Displaying one single article has surprisingly some new challenges. As defined in the application.xml, the Article controller is activated for all request URLs that match the pattern "article/*.html". The asterisk represents a variable part of the URL that has to be matched. With this information the requested article is loadable.

If the article does not exist, the ArticleModel throws an exception. The AbstractMVCController catches all thrown exceptions and lets you handle them in a special error(...) method. The text of the error message should be language-dependent. The final implementation of the Article controller looks like this:

<?php

	class Article extends AbstractMVCController {
		
		protected
			$arrURLParts = array('articleID'),
			$sViewClass = 'SimpleView',
			$bTranslateOutput = true,
			$bAutoloadModel = true;
		
		
		public function display(Request $objRequest, TekunaView $objView) {

			// get the article's ID from the URL
			$arrURLParts = $this -> getApplicationContext() 
			                     -> getURLInformation() -> getMatchedURLParts();
			                     
			$iArticleID = (int) $arrURLParts['articleID'];
			
			// load the model
			$objModel = $this -> getModel();
			$objModel -> loadArticle($iArticleID);
			
			// assign the article data to the view
			$objView -> assign('date', date('Y-m-d H:i:s', $objModel -> date));
			$objView -> assign('title', $objModel -> title);
			$objView -> assign('text', $objModel -> text);
		}
		
		
		public function error(Request $objRequest, TekunaView $objView, 
		                      Exception $objException) {
			
			$objView -> setTemplate('Error.tpl');
			$objView -> assign('message', $objException -> getMessage());
			
			// log exception unless it is an ArticleModelException
			if (! $objException instanceof ArticleModelException) {
				
				log_exception($objException);
			}
		}
	}

The matched URL parts are detected by Tekuna automatically. The $arrURLParts property defines some human-readable names for the URL parts. In this case the one necessary part is called "articleID". The concrete value of this URL part can be selected from the URLInformation (API) object, that is available in the current TekunaApplicationContext (API). The application context contains all request-specific objects such as Request (API), Response (API), ClientInformation (API), URLInformation (API), ApplicationConfiguration (API), the current active langauge and an array of key/value pairs, that can be set freely.

For this controller the default model (ArticleModel) can be used directly. By setting the property $bAutoloadModel to true the model class will be loaded automatically and an instance is made available within the controller. This model is used to load the article by the id that was extracted from the URL.

The article's data are displayed with a SimpleView (API). To handle errors, the view's template is altered and the exception's message is assigned. Exceptions, that are no ArticleModelException are logged to the exceptions.log file. To keep the messages translatable, the property $bTranslateOutput was set to true. The message is not formulated directly in the ArticleModel, but as key (%error.article.does.not.exist%).

<?php

	Tekuna :: load('org.tekuna.core.persistence.TextDB');
	TekunaApplication :: load('components.model.ArticleModelException');

	class ArticleModel implements TekunaModel {
		
		// ...
		
		public function loadArticle($iArticleID) {
			
			// select the article by its ID
			$arrData = self :: getDB() -> selectWhere('id', $iArticleID);
			
			// check the article exists
			if (count($arrData) == 0) {
				
				throw new ArticleModelException('%error.article.does.not.exist%');
			}
			
			$this -> id = (int) $arrData[0]['id'];
			$this -> date = (int) $arrData[0]['date'];
			$this -> title = (string) $arrData[0]['title'];
			$this -> text = (string) $arrData[0]['text'];
		}
		
		// ...
	}

Every strings with the form %key% are extracted from the output and replaced with their translated contents. The translations are loaded automatically from the file "lang/[language]/Article.ini", that looks like this:

error.title=Error
error.article.does.not.exist=The requested Article does not exist.

Generating other output types

Serving an ATOM-feed is a typical example for generated contents, that do not have the mime type text/html. Also images, PDF documents or flash animations require other content types. Changing the content type, HTTP status or adding custom headers is easily possible with the Response object Response (API). The ArticlesFeed controller that generates the ATOM feed for the weblog's articles looks like this:

<?php

	TekunaApplication :: load('components.model.ArticleModel');

	class ArticlesFeed extends AbstractMVCController {
		
		protected
			$sViewClass = 'PHPView',
			$bTranslateOutput = false,
			$bAutoloadModel = false;
		
		public function display(Request $objRequest, TekunaView $objView) {

			// find all articles
			$arrArticles = ArticleModel :: findAll();
			
			// show articles in reverse order
			$objView -> assign('articles', array_reverse($arrArticles));
			
			// change the mime type of the output
			$this -> getApplicationContext()
			      -> getResponse()
			      -> setContentType('application/atom+xml');
		}
	}

The last line of the display(...) method changes the content type by setting a new type at the Response object, that resides in the TekunaApplicationContext.

The MVCController workflow and input data filtering

Writing new articles requires a form to enter the data, a validation of these data with possible validation error messages and the final storing of the new article. This workflow is supported directly by the AbstractMVCController. The display(...) method is called when the controller is executed after a GET request. If any exception occurs, the error(...) method is called where the exception can be handled or re-thrown.

When the controller is executed after a POST request a more sophisticated workflow is triggered. At first, the validate(...) method is called to load and validate the submitted data. If this method returns false, a ValidationException is thrown, that can be handled with the error(...) method. If the validation method returns true, the store(...) method is called, where the validated data may be stored. The implemented AddArticle controller uses this workflow to process new articles:

<?php

	TekunaApplication :: load('components.model.ArticleModel');

	class AddArticle extends AbstractMVCController {
		
		protected
			$sViewClass = 'SimpleView',
			$bTranslateOutput = true,
			$bAutoloadModel = false;
			
		private
			$sMessage = '',
			$sTitle = '',
			$sText = '';
		
		
		public function display(Request $objRequest, TekunaView $objView) {

			$objView -> assign('title', $this -> sTitle);
			$objView -> assign('text', $this -> sText);
			$objView -> assign('message', $this -> sMessage);
		}
		
		
		public function validate(Request $objRequest, TekunaView $objView) {
			
			// set input data filters
			$objComplexFilter = new FilterChain(new UnixLineDelimitersFilter(),
			                                    new NoScriptFilter(),
			                                    new TrimFilter());
			                                    
			$objRequest -> setPOSTFilter('title', $objComplexFilter);
			$objRequest -> setPOSTFilter('text', $objComplexFilter);
			
			// get request data
			$this -> sTitle = $objRequest -> getPOSTValue('title');
			$this -> sText = $objRequest -> getPOSTValue('text');
			
			// validate input data
			if ($this -> sTitle == '') {
				
				$this -> sMessage = '%error.title.empty%';
				return false;
			}
			
			if (strlen($this -> sTitle) > 100) {
			
				$this -> sMessage = '%error.title.length%';
				return false;
			}
			
			if ($this -> sText == '') {
				
				$this -> sMessage = '%error.text.empty%';
				return false;
			}
			
			if (strlen($this -> sText) > 3000) {
				
				$this -> sMessage = '%error.text.length%';
				return false;
			}
			
			// all validations OK
			return TRUE;
		}
		
		
		public function store(Request $objRequest, TekunaView $objView) {
			
			// save article with the model
			$objArticle = new ArticleModel();
			$objArticle -> date = time();
			$objArticle -> title = $this -> sTitle;
			$objArticle -> text = nl2br($this -> sText);
			$objArticle -> save();
			
			// update message and template
			$objView -> assign('message', '%article.saved%');
			$objView -> setTemplate('AddArticleSuccess.tpl');
		}


		public function error(Request $objRequest, TekunaView $objView, 
		                      Exception $objException) {

			// print the exception's message
			if ($this -> sMessage == '') {
				
				$message = $objException -> getMessage();
				$this -> sMessage = "%error.exception|$message%";
			}
			
			// log exception unless it is an ControllerValidationException
			if (! $objException instanceof ControllerValidationException) {
				
				log_exception($objException);
			}
			
			// display the form
			$this -> display($objRequest, $objView);
		}
	}

An important detail is the access to the POSTed values. These values are not available in the superglobal $_POST array. In fact, this array is empty. The values are forced to be filtered by the Request object and can be retrieved from there. Before accessing the values, a proper filter must be set. If this is omitted, the NoSpecialCharsFilter (API). is applied by default. To filter the title and text of a new post, multiple filters are used, that are composed with a FilterChain, that basically executes all the filters one after another. The composed filter first normalizes all line breaks to the UNIX-like way (\n), then removes all kinds of embedded scripts and finally removes leading and trailing whitespaces from the string. For every GET and POST value can be defined another filter.

Another interesting detail is localized error message in the error(...) method. There was used a parametrized translation. This makes it possible to inject variables into translated string. A simple example is using the translatable greeting %greeting.name|Evening|John%. This fragment is replaced by the localization string for "greeting.name" and has the two parameters "Evening" and "John". If the localization string is "Good {1}, {2}." the result will be "Good Evening, John.".

Conclusion

Tekuna is a framework to develop web-based application with PHP 5. With a simple weblog application the features of the framework were presented. The definition of an Tekuna application is performed within the application.xml. There the module, components, URLs, routes, controllers and fragments are defined that build the application. The AbstractMVCController is a simple MVC implementation that supports a simple editing workflow and translatable output. The architecure of Tekuna enfoces the filtering of input data. The consequent usage of exceptions lets you build maintainable applications.

12 Comments

Fri 4 Sep 2009 at 10:14 AM Jim
Tekuna Application Error
The application ended abnormally and the page cannot be displayed.

Message: Function set_magic_quotes_runtime() is deprecated

I am trying to run it under windows as dev environment but seeing it error message.
Fri 4 Sep 2009 at 4:37 PM Niels
You tried to run Tekuna with PHP 5.3. Currently this is not possible. The easiest way is to use a PHP 5.2.x Version. Tonight I will dig a bit deeper into this problem...
Thu 10 Sep 2009 at 3:02 AM Jim
How does it handle Ajax call from JSON or XML data? Seems it enforces me to use controller / view.
Thu 10 Sep 2009 at 3:25 AM display debug message
Seems echo doesn't do anything to client. Any suggestion to do logging? to server or client.
Fri 11 Sep 2009 at 7:22 AM Niels
To produce JSON or XML, you can use the AbstractMVCController. But in this special case, it seems more appropriate to derive directly from AbstractController and just implement the getOutput() method.

It is also possible to write a completely own controller implementation. It just has to implement the TekunaController interface. One of the next releases of Tekuna will have a special controller for JSON and XML output.

The hiding of any outputs made by echo or print_r is an explicit feature of Tekuna. To display theses debug-time outputs, just call the setSendDebugOutputs(true)-method on the Response object. This flag should be FALSE in production.

The next release of Tekuna will have an attribute in the application.xml to set this flag more easy.
Sat 12 Sep 2009 at 1:05 AM Jim
Thanks, It would be good if you can include a default json and xml controller.

I agree with you for hiding echo & print_r for production.
Wed 30 Sep 2009 at 1:12 AM Jim
$objRequest->getPOSTValue(key)
can't get special characters like #, +, &?
Sun 4 Oct 2009 at 7:38 PM Niels
For standard, the Request object uses the NoSpecialCharsFilter[1] for filtering input values. So these characters are removed from the input.

To allow such characters, just change the used filter. The filter you use can be one of the bundled filters or you write your own that has to implement the TekunaFilter[2] interface.

$objFilter = new PrintableCharsFilter();
$objRequest -> setPOSTFiler($key, $objFilter);
$myValue = $objRequest -> getPOSTValue($key);


1: http://tekuna.org/_framework/tools/Tekuna-API/views/frames.php?type=NoSpecialCharsFilter
2: http://tekuna.org/_framework/tools/Tekuna-API/views/frames.php?type=TekunaFilter (including a list of implementing classes: the bundled filters)
Thu 22 Oct 2009 at 3:17 AM Jimmy
Thanks,
One more question. How does session and cookie work in your framework?
Thu 22 Oct 2009 at 8:20 PM Niels
At the moment, both Session and Cookie handling are not covered by Tekuna and must be done with plain PHP.

For the next release (0.3) an abstraction for Sessions is planned[1]. An additional abstraction for cookies seems useful as well ;-) These are some more removed evil superglobal arrays with data that should be filtered...

Please feel free to add comments to the issue on google code with your feature requests or ideas.


1: http://code.google.com/p/tekuna/issues/detail?id=21
Tue 27 Jul 2010 at 10:45 PM Shawn
Hello,

I'm having trouble with the .(application.xml)

I created a example dir and copied your index.php into it.

When I go to http://localhost/web/example/index.php I get nothing. Is there something I'm doing wrong?

I tried to put it under the libs dir but no luck.

Thank you
Shawn
Tue 27 Jul 2010 at 11:09 PM Shawn
Hello,

I'm having trouble with the .(application.xml)

I created a example dir and copied your index.php into it.

When I go to http://localhost/web/example/index.php I get nothing. Is there something I'm doing wrong?

I tried to put it under the libs dir but no luck.

Thank you
Shawn

Add Comment