PHPainfree2 Docs

ApplicationController - Documentation | PHPainfree2

PHPainfree2 ApplicationController

A detailed overview of the PHPainfree2 ApplicationController.

ApplicationController: includes/App.php

The provided App.php makes an excellent starting point for your project. This script, and the singleton you're expected to create, will serve as the overall application state of your program for each and every HTTP request that is passed to the server and handled by PHPainfree2.

ApplicationController
What Where How
Defined PainfreeConfig.php $PainfreeConfig['ApplicationController'];
Called Painfree.php $Painfree->logic();
PHPainfree/
|-- htdocs/
|-- includes/
|   |-- PainfreeConfig.php
|   |-- Painfree.php
|   |-- App.php
|   `-- Controllers/
`-- templates/

class App Overview

The provided ApplicationController is a stripped down version of several production ApplicationControllers that have been developed for several large companies serving hundreds of thousands of requests for complicated highly-interactive data-driven web applications. This file is an excellent starting point for projects of all sizes, ranging from quick Hackathon projects to high-availability production web applications.

Environment Setup - includes/App.php

At the top of the ApplicationController, the code creates a global Singleton instance of class App defined in this file.

Reminder

This ApplicationController is just code that is automatically executed by $Painfree->logic() and DOES NOT have to be fully defined in this file. Some products are better suited having the ApplicationController class stored in a different file and defined with proper namespacing, or perhaps loaded automatically through a PSR-4 composer autoload mechanism.

Build things to suit YOUR needs, not the framework's needs.

Eric Harrison, PHPainfree Structure Documentation
includes/App.php

<?php
	// It's common to rename the "App" class and object instance to match
	// your specific product. Feel free to leave it as $App or rename it.
	$App = new App();
				
includes/App.php

		class App {
		// snipped...

		public function __construct() {
			global $Painfree;

			$this->BASE_PATH = str_replace('/htdocs', '', $_SERVER['DOCUMENT_ROOT']);

			// $this->db = $Painfree->db;
      // OR
			// require_once 'core/MySQLiHelpers.php';
			// $this->db = new MySQLiHelpers($Painfree->db);
			
			// Set up the route and prepare for routing
			$this->route = $Painfree->route;
		}
				
Other Classes

Directly below the instantiation, you can see a few lines commented out with a recommendation for how to build these types of ApplicationControllers.

A common PHPainfree2 design pattern is to attach several other required object Singletons to the primary ApplicationController Singleton. In the example code that is commented out, a class User singleton is imported and instantiated as a public member of the $App instance.

Because this example ApplicationController uses the $App instance as the "application state" and execution director, it would be typical to have several such instances defined and attached to the $App instance at run-time. There are several such pre-defined example classes available in this repository, and you can learn more about them at /docs/painfree-examples.

includes/App.php

	$App = new App();
	// any internal classes should be defined below
	// require_once 'includes/App/User.php'; 
	// $App->User = new User();
	
	// start routing and handle the request
	$App->route();
				

$App->route()

Finally, on line 10, a tiny bit of magic happens with the call to $App->route(). This code, defined inside the class is an example of an ApplicationController consuming the structured route of $Painfree->route to dynamically handle requests and pass them to the correct controller.

Using this function, or one like it, allows PHPainfree2 projects to orchestrate complex behaviors at URLs defined entirely by the name of the first value in the URL path.

includes/App.php

	// start routing and handle the request
	$App->route();

	/**
	 * App Singleton
	 */
	class App {
		// ... 
				

$App->route() ➡️ $this->__setRoutes()

Inside $App->route(), the first thing we do is call the __setRoutes() private method. This function takes the route that PHPainfree2 has defined and splits it up into various individual components. This ApplicationController has defined a routing format as: /$VIEW/$ID/$ACTION, so __setRoutes() examines $Painfree->route and assigns each piece to public member variables in $App

includes/App.php, App::setRoutes()

		private function __setRoutes() : void {
			$routes = explode('/', $this->route);

			// default route is set in PainfreeConfig.php
			if ( count($routes) ) {
				$this->view = array_shift($routes);
			} else {
				$this->view = $this->route;
			}

			if ( count($routes) ) {
				$this->id = array_shift($routes);
			}

			if ( count($routes) ) {
				$this->action = array_shift($routes);
			}
		}
				
includes/App.php

		/**
		 * $App->route() takes the $Painfree->route value and attempts
		 * to load a controller from `/includes/Controllers/`.
		 *
		 * @return void
		 */
		public function route() : void {
			global $Painfree;
			
			// the default routing pattern is
			// /:VIEW/:ID/:ACTION
			$this->__setRoutes();
			
			header('X-Frame-Options: SAMEORIGIN');
				

"Magic" Routing

Now that the route variables have been prepared, the route() method does the next four pieces of magic in a few short lines of code.

  1. Check if there is a controller script defined for $App->view.
    • If Controller Exists, load and execute the controller script.
    • If No Controller, send a HTTP 404 status code.
  2. htmx Support [partials] - Check the Request Headers to see if HX-Request is set and set a boolean for our templates to use.
  3. htmx Support [boosted links] - Check the Request Headers to see if HX-Boosted is set and set a boolean for our templates to use.
  4. Fast API Support - Check the request headers to see if Content-Type is set to "application/json" or a query parameter of ?json was sent with the URL, and if so, set the response Content-Type and immediately die and send back the contents of $this->data encoded as JSON.
includes/App.php

			header('X-Frame-Options: SAMEORIGIN');

			if (file_exists("{$this->BASE_PATH}/includes/Controllers/{$this->view}.php")) {
				require_once "Controllers/{$this->view}.php";

				$headers = apache_request_headers();
				if ( isset($headers['HX-Request']) && 
					$headers['HX-Request'] === 'true' ) {
					$this->htmx = true;
				}
				if ( isset($headers['HX-Boosted']) && 
					$headers['HX-Boosted'] === 'true' 
				) {
					$this->htmx_boosted = true;
				}
				if ( isset($headers['Content-Type']) && 
					$headers['Content-Type'] === 'application/json' ) {
					header('Content-Type: application/json; charset=utf-8');
					die(json_encode($this->data));
				}
			} else {
				header('HTTP/1.0 404 Not Found');
				// We don't "die" here so that the developer can render a
				// nice looking 404 template if they want.
			}
				

Rendering a Template BaseView

After completing the route() method, execution returns to $Painfree and $Painfree->view(); is called. This function loads the script defined in your PainfreeConfig.php BaseView option, which is the script located in templates/. In the PainfreeConfig provided with this project, the initial BaseView template is defined as templates/app.php.

Your initial BaseView template is often going to be the most complicated template in your project, performing double-duty as an HTML skeleton for the rest of your views as well as dynamically loading view templates based on the URL. In PHPainfree2, this template also has code to handle htmx partial templates.

PHPainfree/
|-- htdocs/
|-- includes/
`-- templates/
    |-- app.php
	`-- views/
	    `-- main.php
templates/app.php

<?php
if ( 
	$App->htmx && 
	! $App->htmx_boosted && 
	file_exists("{$App->BASE_PATH}/templates/views/{$App->view}.php") 
	) {
	// If we are an htmx request and the "view" variable exists in the top-level
	// templates folder, render that as an HTMX snippet.
	//
	// If we are an htmx request and there is a "sub-view" defined that lives
	// inside a folder, render _THAT_ instead of the full top-level snippet.
	//
	// In _this_ application, we're overriding $App->id to act as our default
	// "sub-view" route, but you should feel free to write whatever type of 
	// routing architecture that you want.
	//
	// This example requires that a top-level /templates/views/{$view}.php file 
	// exists **AND** a top-level /templates/views/{$view}/{$id}.php file to
	// exist for this magic to occur. 
	//
	// Each application built with PHPainfree should design their routing and
	// template relationships however best suits that product.
	$file_path = "{$App->BASE_PATH}/templates/views/{$App->view}/{$App->id}.php";
	if (file_exists($file_path)) {
		include_once "views/{$App->view}/{$App->id}.php";
	} else {
		include_once "views/{$App->view}.php";
	}
} else { 
?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<title><?= $Painfree->safe($App->title()); ?></title>

		<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />

Dynamic View Template Loading

Inside of templates/app.php in the meat of our web template, we'll load a template file in the template/views/ directory if one exists with the same name as the value stored in $App->view.

In PainfreeConfig, we have our DefaultRoute parameter defined as "main", so our application will serve templates/views/main.php for any request to either http://hostname.com/ (no path) as well as explictly called like http://hostname.com/main. The value of that view, "main" automatically loads:

  • includes/Controllers/main.php
  • templates/views/main.php

This is one of the designs that makes developing projects with PHPainfree2 so quick. Adding new pages is as simple as dropping a file in the includes/Controllers/ folder and templates/views/ folder. And as you develop more complicated applications, allowing all of your Controller code to serve as the business logic for your REST JSON API, you're able to do a lot more with a lot less duplication.

templates/app.php

	<body id="app-body" class="bg-dark text-light">
<?php
		include 'header.php';	
?>

<?php
	if ( file_exists("{$App->BASE_PATH}/templates/views/{$App->view}.php") ) {
		include "views/{$App->view}.php";
	} else {
		include "views/404.php";
	}
?>

<?php
		include 'footer.php';	
?>