Skip to main content

Fat-Free Framework

Selecting, installing, and structuring the Fat-Free Framework:

Language detection, selection, and support:

Going with the Fat-Free Framework

It was a 2018 review of PHP frameworks that listed Fat-Free with 12.5% of the market and noted that Fat-Free has a “small learning curve” that persuaded me to try it. In 2023, Fat-Free is still one of the top rated PHP frameworks.

The most popular PHP frame work is Laravel, but two reasons (besides the small learning curve) that persuaded me to go with Fat-Free over Laravel are: Laravel requires PHP 8.0 or higher, while Fat-Free (version 3.7) can run on PHP 5.6 or higher; and Laravel requires the command-line package manager utility, Composer, for installation. Fat-Free can be installed using Composer or directly.

I don’t use Composer as I don’t want to depend on being able to get command-line access to the server, and I’m leery of package managers as I want to control when updates happen.

*Note: After selecting Fat-Free, I found instructions for using Laravel on a shared host, but at that point, I was already well along with using Fat-Free.

Installing the Fat-Free Framework

The Fat-Free distribution package comes with Fat-Free and some demo files. To install Fat-Free, I copy Fat-Free’s lib directory to the server and create the directories for my files:

  • root – for index.php and .htaccess
    • app – for the configuration files
      • ui – for HTML template files (as a subdirectory under the app directory)
    • css – for the website’s CSS files
    • dict – files for each language with key/value pairs for translated text
    • images – for the website’s image files
    • js – for the website’s JavaScript files
    • lib – directory for the Fat-Free files

I have a copy of Fat-Free on my PC running under WAMP using the same structure for development with the addition of a Sass directory under the CSS directory and the node_modules directory (which is created by npm when installing Bootstrap 5) under the root directory.

Site structure

Many websites use a Model-View-Controller (MVC) pattern with the data for the pages in the model (often in a database).

This site does not utilize a model. Page content is coded directly into the templates (the views) and the controllers, rather than being stored in a database. As the content generally does not change, there is no benefit for defining a storage and retrieval structure for data and then entering the data into a database. It is easier to deal directly with the html style templates.

This site follows (generally) a Supervising Controller Pattern as defined by Derek Greer in a paper published online in 2007 (the paper is now only available through the Internet Wayback Machine). A routing function defined by routes.ini sends user requests for pages to the correct PHP controller class, which assembles the required template files with values stored in the controllers to build and deliver the webpage to the user.

Diagram of the supervising controller pattern used to organize the site

Values that are common to multiple pages are stored in a config.ini file. This makes the information easy to see and modify, and it helps eliminate the need for a database (along with the code to support one).

Using files (rather than a database) does require discipline in adhering to the site logic and for entering the same information multiple times (e.g. the page slug), but if there is a typo, a page won’t load correctly, so the error can be found and corrected.

The structure for using information from the config.ini and routes.ini files is illustrated below:

A) Language prefix

Each language group has the same structure (same number of pages and in the same order) and each component starts with the language prefix, which is defined in the language list.

B) Page slug

The slugs that define the URL for each page are defined in the navigation (dropdown) menu and copied to the routes.

C) Route name

The route name is defined in the route and used as the name of the HTML file for that page. The base route name (the “_details1” part) is the same across all languages.

D) Controller function

The route names from the controller function are used in the controller file.

URL organization

Requests for the root directory are rerouted to one of the language subdirectories.

The URLs for the pages in each language subdirectory are organized in the same way:

https://sbf-bootstrap5.alwaysdata.net/en/home
https://sbf-bootstrap5.alwaysdata.net/en/bootstrap
https://sbf-bootstrap5.alwaysdata.net/en/fatfree
https://sbf-bootstrap5.alwaysdata.net/en/ffconfig
https://sbf-bootstrap5.alwaysdata.net/en/templates
https://sbf-bootstrap5.alwaysdata.net/en/details
https://sbf-bootstrap5.alwaysdata.net/en/contact

The navigation includes same-page links for the home page:

https://sbf-bootstrap5.alwaysdata.net/en/home#home
https://sbf-bootstrap5.alwaysdata.net/en/home#overview
https://sbf-bootstrap5.alwaysdata.net/en/home#bootstrap
https://sbf-bootstrap5.alwaysdata.net/en/home#fatfree

Note: A single page website would include same-page links in the navigation, but a multi-page site might not. I included both in the navigation, not as a recommendation for how to setup a site, but to be sure I could do both methods.

I would have preferred to not have needed to designate one page as home, but, unfortunately, Fat-Free does not support a slash at the end of the URL:

https://sbf-bootstrap5.alwaysdata.net/en/ – with a trailing slash – not supported
https://sbf-bootstrap5.alwaysdata.net/en – without a trailing slash – supported

If a request is made for a URL with a trailing slash, Fat-Free will do an HTTP 302 redirect to the address with the slash removed.

There was a discussion on GitHub in 2013 around trailing slashes, with the decision being to keep Fat-Free as it was (continue removing the trailing slash in the URL).

In that discussion, there was some concern that having both URLs as valid would cause problems with search engines such as Google (the concern seemed to be that having the same content at the different URLs would impact the page’s search engine ranking).

For reference, it is possible to modify Fat-Free’s code to remove the slash by commenting out part of the code in base.php:

if (isset($route[$this->hive['VERB']]) && !$preflight) {
/* Comment out the if statement
if ($this->hive['VERB']=='GET' &&
    preg_match('/.+\/$/',$this->hive['PATH']))
    $this->reroute(substr($this->hive['PATH'],0,-1).
        ($this->hive['QUERY']?('?'.$this->hive['QUERY']):'')); */
list($handler,$ttl,$kbps,$alias)=$route[$this->hive['VERB']];

While I don’t like having the URL /en/home#home, modifying Fat-Free comes with three drawbacks:

  1. I would need to make this modification every time there’s an update to the Fat-Free code.
  2. I would need to include routes for the trailing slash version of each URL in routes.ini as Fat-Free would consider those as valid URLs.
  3. And most importantly, it would create extra work when configuring a new site (sometimes it’s important to get a new site configured quickly so a client can see the content right away).

Preferred language

Requests for the root directory are redirect to a language subdirectory. Determining which subdirectory is done by comparing the Accept-Language string from the browser’s request header with the languages available on the site.

/ (root directory) route

A GET request for the root directory calls the getLanguage function in UtilitiesController.php:

function getLanguage($f3) {
    $f3->reroute('@'.\Preflang::instance()->langDirectory($f3).'_home');
}

That function calls the langDirectory function in the Preflang class, which checks if there is a match between the languages available on the site and the languages the user prefers, and returns the subdirectory URL for the best match. The getLanguage function then does a 301 reroute to send the user to the appropriate subdirectory

Preflang->lang­Directory() function operation

The function uses the language definition array from config.ini and the accept-language string from the browser to make the language selection:

1 – Language definition array from config.ini:

[languages]
en=en,en,English,default
zh=zh-CN,zh,中文(简体),Simple Chinese
ko=ko,ko,한국어

Each line in the array represents one language that the site supports:

  • en: the array key and the language subdirectory – www.mysite.com/en/
  • en: the two-letter ISO 639-1 language code or the two-letter language code with a two-letter ISO-3166 country or territory code (e.g. zh-CN)
  • en: only the two-letter ISO 639-1 language code (no territory code)
    • The Accept-Language string will often list first the user’s preferred language with a territory code, followed by just the language code
    • The language code will often be the same as the array key, but in some cases it will be different (e.g. Hong Kong – hk for the subdirectory and zh for the language code). I use the two-letter version in checking for a user’s preferred language.
  • English: the language name (for the language menu)
  • default: reference note (I don’t use this field in the code)

2 – Accept-Language string from the browser’s request header

The Accept-Language string specifies the languages a user can accept: en-US, en; q=0.7, ko; q=0.3. (People have the option to configure their browser, but most don’t.)

The country code is for offering territory specific versions. This is important for Chinese language versions as China uses Simplified Chinese while Hong Kong and Taiwan use Traditional Chinese. By listing the different codes, the system can deliver the right page (if it’s available, otherwise it will serve a default zh Chinese language page).

3 – Language selection

The langDirectory function converts the Accept-Language string into an array of language groups using the PHP preg_match_all function. For the Accept-Language string en-US,en;q=0.5, the function returns:

array (
    array('en-US','en','-US','US'),
    array('en','en'),
);

The language values (the first value in each language group array) are compared to the language definitions array.

If there is a match, the function returns the directory prefix for the matching language, otherwise it returns the default language prefix (which is the first value in the language definitions array).

Language selector

Language dropdown menu screen capture

The language selector displays a dropdown menu of links to corresponding pages in other languages.

The selector is always shown in the navbar so users will be aware that there are other language versions available. For large sites like Google or Wikipedia, people assume the site supports other languages, so they’ll go looking for the language selector. For a small site, people may not make the same assumption, so it’s a good idea to have the language selector visible.

The language selector is built from an array that is created by Controller.php’s afterroute function:

array (
    ('en_details1','en','en','English'),
    ('ko_details1','ko''ko',,'한국어'),
    ('zh_details1','zh''zh-CN',,'中文(简体)')
)

The array has the route name, subdirectory name (for loading the flags), language code with territory (for the hreflang links in the head section), and language name for each language. The array is used by the head.html template to create the hreflang values and the navigation.html template to create the language dropdown menu:

<div class="dropdown navbar-nav lang-dropdown hide-for-ie">
    <button class="btn btn-link nav-link dropdown-toggle border-0"
        type="button" id="navbarDropdownLanguageMenu"
        data-bs-toggle="dropdown" aria-expanded="false"
        aria-label="{{ @dictNavLanguage }}">
        <span class="flag-icon flag-icon-lg flag-icon-{{ @langSubdirectory }}">
            </span>
    </button>
    <div class="dropdown-menu dropdown-menu-start dropdown-menu-md-end slide-in">
<repeat group="@languageLinks" value="@link">
        <a class="dropdown-item 
            {{ @link[1]===@langSubdirectory ? ' current-language' : '' }}" 
            href="{{ @schemeHost }}{{ @link[0] | alias }}">
            <span class="flag-icon flag-icon-{{ @link[1] }}"></span>{{ @link[3] }}
        </a>
</repeat>
    </div>
</div>

Fat-Free’s built-in language support

Fat-Free supports multiple languages through dictionary files. Fat-Free selects the dictionary file from which to retrieve the required text string based on the LANGUAGE variable.

While the content for the site’s main pages is coded directly in each language, I use Fat-Free’s language support for pages that have just small amounts of text and that can use the same template file. These are the:

  • Contact page
  • Page not found page
  • Error page

I also use the dictionaries for phrases that are common to all pages of a language, but are different for each language (e.g. the title for the scroll-to-top button) and for the footer content.