framework4

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

  1. Extract the contents of src/ into a directory of your choosing.
  2. Make sure the webserver has write privileges to system/data/
  3. Create a mySQL/Mariadb database
  4. Fill out app/config/store/db/main.php
  5. 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 ([...]);");

The framework comprises a set of utility functions to handle queries with convenience.

001  $db = FrameworkStoreManager::get()->store();
002  
003  # insert data
004  $db->insert_into(<tablename>);
005  $db->set(<columnname>, <typeident: i|d|s>, <value>);
006  $insert_id = $db->run();
007  
008  # update data
009  $db->update(<tablename>);
010  $db->set(<columnname>, <typeident: i|d|s>, <value>);
011  $db->where(<columnname>, <operator: =|<|>|<=|>=|LIKE ...>, <typeident: i|d|s>, <value>);
012  $db->where(<columnname>, <operator: =|<|>|<=|>=|LIKE ...>, <typeident: i|d|s>, <value>); // AND OR
013  $db->or_where(<columnname>, <operator: =|<|>|<=|>=|LIKE ...>, <typeident: i|d|s>, <value>);
014  $db->run(); // true if update successful
015  
016  # prepared queries
017  $sql = "SELECT * FROM `<tablename>` WHERE `id` = ? AND `user_id` = ?";
018  $res = $db->pquery($sql, 'ii', $id, $userid);
019  // returns a mysqli result object, therefore
020  $res->num_rows    // number of results
021  $res->fetch_assoc // etc. to fetch results
022  // if values are passed as array
023  $db->pquery($sql, 'ii', ...$array);

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 locale 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>