A MVC framework written in PHP. Designed to be simple to set up and maintain with as few dependencies as possible.
WORK IN PROGRESS
History
- YYYY-MM-DD (0.0.0): WIP
System Dependencies
- PHP 7.3+
- A webserver with support for URI rerouting
Installation & Setup
- Extract the contents of
src/
into a directory of your choosing. - Make sure the webserver has write privileges to
system/data/
- Create a mySQL/Mariadb database
- Fill out
app/config/store/db/main.php
- Run
php scripts/migrate.php app/migrations <database-key>
The framework comes with .htaccess files to run on an apach2 webserver. To work on other servers the following configuration has to be set up:
- Reroute all URIs not matching any files and directories to
/index.php
- Restrict access to
/system*
,/app*
and/scripts*
User Documentation
A basic guide on using the framework.
Config Files
Remember to be careful with database credentials when publishing your code. These can be set according to environment variables.
Application config values are accessible via AppConf
:
001 $some_conf_value = AppConf::get('<version|environment|modules|available_languages|default_language>');
002
003 # app/config/app.conf.php
004 # Example
005
006 # Current version of the software (not the framework's version
007 $config['version'] = '1.0.0';
008
009 # Leave empty ("") if app is installed to website's document root
010 # If it's in a subdirectoy, that path is required here. No leading or terminating slashes!
011 $config['root_dir'] = "some_dir";
012
013 # Used to prefix cookie names
014 $config['name'] = "my app";
015
016 # Framework::ENV_DEV for development
017 # Framework::ENV_PRD for production
018 $config['environment'] = Framework::ENV_DEV;
019
020 # Currently not used, leave empty
021 $config['modules'] = array(
022 #'auth'
023 );
024
025 # IETF language tag mapping for languages supporting many tags
026 # also a list of languages supported by the software
027 $config['available_languages'] = array(
028 'en' => 'en',
029 'en-US' => 'en',
030 );
031
032 # Default language used by the software when no user information is available
033 $config['default_language'] = 'en';
034
035 # app/config/store/db/<db-key>.conf.php
036 # Example
037
038 # Database information
039 $config['name'] = '<db>';
040 $config['key'] = '<key>'; # used in code to identify this database!
041 $config['type'] = 'mysql/mariadb'; # only type currently supported
042 $config['engine'] = 'InnoDB';
043 $config['charset'] = 'utf8mb4';
044 $config['collate'] = 'utf8mb4_unicode_ci';
045 $config['default'] = true;
046
047 # List of connections to this database
048 $config['connections']['<conn-key>'] = array(
049 'key' => '<conn-key>',
050 'host' => 'localhost',
051 'port' => '',
052 'user' => '',
053 'pswd' => '',
054 'default' => true,
055 'ssl' => array(
056 'active' => false,
057 'key' => null,
058 'cert' => null,
059 'cacert' => 'path',
060 'capath' => null,
061 'cipheralgos' => null
062 ),
063 'options' => array(
064 MYSQLI_OPT_CONNECT_TIMEOUT => 10,
065 MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, false
066 ),
067 );
068
069 // You can configure multiple connections by setting the same keys
070 // in another array in $config['connections']
071 // $config['connections']['<conn2>'] = array(
072 // 'name' = 'Alternative Connection';
073 // ...
074 // );
Routing
app/config/routes.conf.php
defines uri to controller->action mapping.
Supported methods are post
, get
, delete
and patch
. URI segment tokens starting with :
designate parameters, their values are accessed in code via the keys denoted in the route. Example: page/profile/id/:id
maps to URIs such as https://www.example.com/page/profile/id/22
. Regex may be used to restrict matching URIs for parameters: page/profile/id/:id{[0-9]+}
.
If no match is found the application jumps into the controller and action defined by FrameworkRoute::set_default('<controller>#<action>');
.
Framework4 uses Rails' convention that expects controller names to be all lowercase with underscores before the actual class introduces an uppercase letter: MainController
-> main
, UserProfileController
-> user_profile
. A #
separates controller name from action name.
FrameworkRoute::set('<post|get|patch|delete>', '<token>/<token>(/<token>...)', '<controller>#<action>');
FrameworkRoute::set_default('<controller>#<action>');
hint make sure to delete system/data/routes.txt
each time after updating app/config/routes.conf.php
on the server for the changes to reflect in the application.
<< TODO >>
- adjust controller name matching
- sort internal route token arrays to hold wildcards last
Migrations
Are used to run database updates. Run php scripts/migrate.php <dir> <db-key>
to run all SQL
files in <dir>
for all connections defined for <db-key>
in the database's config file. Migration files are required to be prefixed with a timestamp for the script to run them in the correct order:
touch "$(date +%s)_<filename>.sql"
.
Controllers
Controller classes are placed in app/controllers/
and are conventianally suffixed with Controller
. Actions make use of services and create the Views that transform output for display.
001 class ExampleController extends FrameworkControllerBase {
002
003 private $some_service;
004
005 public function __construct()
006 {
007 $this->some_service = new SomeService($this);
008 }
009
010 public function example_action()
011 {
012 $this->some_service->do_stuff();
013 $view = new ExampleView($this);
014 $view->some_method();
015 }
016
017 }
By inheriting from FrameworkControllerBase
, all controllers can access the Request singleton through $this->request
.
Model
Services
app/model/services/
for classes used by controllers to work business logic.
Data
app/model/data/
for classes representing data (the framework does not supply a ORM).
Views
app/views/
001 class ExampleView extends FrameworkViewBase {
002
003 public $some_property;
004
005 public function prepare_and_display_data_for_some_action()
006 {
007 $this->some_property = <whatever>;
008 $this->set_layout('<filename>');
009 $this->render('<filename>');
010 }
011 }
012
013 # renders file passed to the first call to `render()` in a view method
014 public function render_content();
015
016 # renders template files | optional array is expected to
017 # associate values to keys starting with letter symbols.
018 public function render($filename, $data = array());
Layouts and Templates
app/templates/
001 <!DOCTYPE html>
002 <html>
003 <head>
004 <?php $this->view->render('head.html.php'); ?>
005 </head>
006 <body>
007 <?php $this->view->render_content(); ?>
008 </body>
Each call to $view->render()
creates an object for a template with access to its view and its own data sent by render()
.
001 # access view properties
002 $this->view->some_prop
003 # access data sent to this object via render
004 $this->key
005
006 # recommended for links
007 $this->base_uri('page/profile');
Using a Database
The singleton class FrameworkStoreManager
handles global access to databases.
001 $db_key = <optional key defined in db conf>;
002 $conn_key = <optional key defined in connection conf>;
003 $conn = FrameworkStoreManager::get()->store($db_key)->connection($conn_key)->get();
004
005 # $conn may now be used to access a mysql database
006 mysqli_query($conn, "CREATE TABLE example ([...]);");
Global Data
Request
Singleton class that holds request data.
001 $request = FrameworkRequest::get();
002
003 # Publicly accessible properties
004 $request->useragent;
005 $request->method;
006 $request->address;
007 $request->https_f; # boolean flag that shows whether request occured via ssl/tls
008 $request->uri;
009 $request->uri_elements;
010
011 # For multilingual applications
012 # This method has to be run once for $request->acclang to hold values
013 $request->init_accepted_languages();
014
015 # Array of IETF language tags sent with the request, in descending order of weight
016 $request->acclang;
<< TODO >>
- Validate inputs
- Set values to
false
if unexpected data was sent
Session
Do not use $_SESSION
directly. Instead use SessionModule
classes:
001 class SessionData extends FrameworkSessionModule {
002 use FrameworkMagicGet;
003 private static $magic_get_attr = array('a', 'b', 'c');
004
005 protected $a;
006 protected $b;
007 protected $c;
008
009 public function __construct()
010 {
011 $this->a = 1;
012 $this->b = 2;
013 $this->c = 3;
014 }
015 }
016
017 $data = new SessionData();
018 # store object in session accessed via <key>
019 $data->register(<key>);
020 # retrieve object elsewhere
021 $my_data = FrameworkSession::get()->get_module(<key>);
022 # remove object from session and null its properties
023 $my_data->unregister();
Cookies
Are defined in app/config/cookies.conf.php
. For example:
001 $cookies['framework-language'] = array(
002 'value' => null,
003 'duration-days' => 365,
004 'duration-hours' => 0,
005 'duration-minutes' => 0,
006 'duration-seconds' => 0,
007 'domain' => $_SERVER['SERVER_NAME'],
008 'tls-only' => true,
009 'http-only' => true,
010 'samesite' => null # Lax, None, Strict
011 );
They are loaded into FrameworkRequest
on startup and can be accessed anywhere:
001 $request = FrameworkRequest::get();
002 $cookie = $request->cookies['some_key'];
003 $cookie_value = "123";
004 $cookie->set($cookie_value);
005 $cookie->unset();
The samesite
attribute is only used with PHP versions 7.3.0
and above. Cookies are stored as their keys prefixed with the application name defined in app/config/app.conf.php
. Their properties are public and can be changed whenever before calling ->set()
;
Language & Locales
framework4 uses IETF language tags to identify the language to be used for a request.
001 $lang = FrameworkLanguage::get();
002 $lang->from_default();
003 $lang->from_browser(); # requires $request->init_accepted_languages();
004 $lang->from_cookie();
005 $lang->update($tag); # updates language tag in use and writes it to a cookie
006
007 # Controllers have a shortcut that calls the above method chain:
008 class MyController extends FrameworkControllerBase {
009
010 public function __construct()
011 {
012 # setup language tag
013 $this->init_language();
014 # retrieve tag
015 $this->lang->tag # e.g. "en-US"
016 }
017 }
How to set up a locale:
Create a mapping in app/config/locale.conf.php
001 # IETF Language Tag -> Locale Class Name
002 $config['locales']['en'] = 'LocaleEn';
Create a locale interface in app/locales/
and fill it with the contents of system/locales/FrameworkLocaleInterface.php
001 interface LocaleInterface {
002
003 public function ...(...);
004
005 }
Create local files in app/locales/
matching the class names specified in app/config/locale.conf.php
001 # app/locales/LocaleEn.php
002
003 class LocaleEn implements LocaleInterface {
004 use FrameworkLocaleEn; # Implements interface system/locales/FrameworkLocaleInterface
005
006 public function ...(...);
007
008 }
Using locales:
001 <title>
002 <?php echo Framework::locale()->some_function(<parameter>,...); ?>
003 </title>