Selecting, installing, and structuring the Fat-Free Framework:
Language detection, selection, and support:
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.
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 filesdict
– files for each language with key/value pairs for translated textimages
– for the website’s image filesjs
– for the website’s JavaScript fileslib
– directory for the Fat-Free filesI 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.
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.
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.
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:
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.
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
The function uses the language definition array from config.ini and the accept-language string from the browser to make the language selection:
[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:
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).
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).
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 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:
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.