The Alpha Framework is a full-stack MVC framework for PHP. It provides the following features:
The current latest stable release is tested via unit testing and manual testing, and is considered suitable for production usage.
Alpha should be installed using Composer. Here is a minimum example composer.json file to install the current release:
{
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"alphadevx/alpha": "4.*"
},
"autoload": {
"psr-0": {
"": "src/"
}
},
"scripts": {
"post-update-cmd": [
"cp -R vendor/twbs/bootstrap/dist/* public/bootstrap"
],
"post-install-cmd": [
"rm -rf public/bootstrap",
"mkdir -p public/bootstrap",
"cp -R vendor/twbs/bootstrap/dist/* public/bootstrap"
]
}
}
Alpha stores its records in a relational database, currently MySQL and SQLite are supported via injectable providers (more will be written in the future). For SQLite, a file will be written to the filesystem based on your configuration (see the next section), but for MySQL you will need to manually create the database first:
MariaDB [(none)]> CREATE DATABASE alphaframework DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
Query OK, 1 row affected (0.00 sec)
You should also ensure that the MySQL user account that you are going to use to access that database has the correct rights granted to it.
Alpha configuration is broken down into three deployment envrionments, which in turn have their own config files:
Name | Description | Config file |
---|---|---|
dev | Your developer environment (localhost). | config/dev.ini |
test | Your test environment for QA/UAT testing. | config/test.ini |
pro | Your production environment. | config/pro.ini |
From the vendor directy created by Composer, you should copy those files into your application root directory:
$ cp -R vendor/alphadevx/alpha/config .
Alpha chooses the correct file to load based on the hostname of the current server. That hostname to config environment mapping occurs in the config/servers.ini file. Here is an example of that file:
; The server names for the three environments
dev = localhost,alphaframework.dev
pro = alphaframework.org
test = unittests,qa.alphaframework.org
Multiple server hostnames can be added per environment, seperated by commas.
The config files themselves are well-commented, but here is a list of the minimum settings you should configure before continuing with your installation:
Setting | Description |
---|---|
app.url | System root URL. |
app.root | The OS root directory. |
app.file.store.dir | The path to the directory where files are stored (logs, attachments, cached files etc.). |
app.title | The title of the web site. |
db.name | The name of the main database to use. |
db.username | Database username. |
db.password | Database password. |
db.hostname | Database host. |
security.encryption.key | Secret key used for encryption when using the SecurityUtils class. |
app.install.username | Username used for admin access during intallation when creating the database. |
app.install.password | Password used for admin access during intallation when creating the database. |
Please review the comments in the config files for addition optional settings.
Alpha uses a front controller to route all traffic to a single physical file in your web server configuration. That file, index.php, is contained with the public directory of the framework. Copy that directory into the root of your application:
$ cp -R vendor/alphadevx/alpha/public .
Now open that file in your editor to review: it is this file that you will use to add in customer routes to your custom controllers. You will then need to configure your web server to send all traffic to your public/index.php file (with the exception of static files that are also in public), here is an example Apache virtual host configuration:
<VirtualHost alphaframework.dev:80>
DocumentRoot "/home/john/Git/alphaframework/public"
ServerName alphaframework.dev
<Directory "/home/john/Git/alphaframework/public">
Options FollowSymLinks
Require all granted
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/index\.php -f
RewriteRule ^(.*)$ /index.php?page=$1 [QSA]
</Directory>
</VirtualHost>
Once you have configured your application, your web server, and created an empty database, you should then start up you web and database servers and navigate to the root URL of your application. Alpha will automatically detect that it is not installed, and will re-direct to the Alpha\Controller\InstallController for you.
The first thing you will see is a prompt for a username and password, you should enter the values for app.install.username and app.install.password from your application config file to gain access.
The InstallController will create any directories required for you (to store log files, the file cache, article attachments etc.), in addition to creating the database tables, indexes, and foreign keys required to support our active records. Once all of this work is completed, a link to the administration backend will be presented.
Once your application is installed in production, you should set the app.check.installed config value to false to remove the overhead of checking to see if the application is installed on each request.
The model layer of Alpha consists of an active record implementation, and a set of data types. The data types are designed to map to corresponding database types seamlessly, as well as providing validation and frontend widgets. The following data types are included:
Type | Size | Validation rules | Description |
---|---|---|---|
Alpha\Model\Type\Boolean | 1 | 1/0 or true/false. | A boolean value. |
Alpha\Model\Type\Enum | - | One of the items from the list. | A fixed listed of strings to choose from, e.g. weekdays, gender etc. |
Alpha\Model\Type\DEnum | - | ID for one of the items from the list. | A Dynamic Enum, allows you to modify the elements in the enum as stored in the database. |
Alpha\Model\Type\Date | 10 | A date in ISO YYYY-mm-dd format. | A date value. |
Alpha\Model\Type\Timestamp | 19 | A timestamp YYYY-mm-dd hh:ii:ss. | A timestamp value. |
Alpha\Model\Type\Double | 13 | A valid double value. | A double value. |
Alpha\Model\Type\Integer | 11 | A valid integer value. | An integer value. |
Alpha\Model\Type\Relation | 11 | ID of the related record, or empty. | A relation type supporting MANY-TO-ONE, ONE-TO-MANY, ONE-TO-ONE, MANY-TO-MANY types. |
Alpha\Model\Type\Sequence | 255 | Sequence in format PREFIX-00000000000. | A database sequence number with a string prefix defined by the developer. |
Alpha\Model\Type\SmallText | 255 | A small text value. | A small text value (single line). |
Alpha\Model\Type\Text | 65535 | A large text value. | A large value (paragraphs). |
An active record then uses these data types to store the attributes of an object we care about in the database. For example, here is how our Person active record makes use of these complex types:
<?php
namespace Alpha\Model;
// ...
class Person extends ActiveRecord
{
/**
* The forum display name of the person.
*
* @var \Alpha\Model\Type\SmallText
*
* @since 1.0
*/
protected $username;
/**
* The email address for the person.
*
* @var \Alpha\Model\Type\SmallText
*
* @since 1.0
*/
protected $email;
/**
* The password for the person.
*
* @var \Alpha\Model\Type\SmallText
*
* @since 1.0
*/
protected $password;
// ...
/**
* An array of data display labels for the class properties.
*
* @var array
*
* @since 1.0
*/
protected $dataLabels = array('ID' => 'Member ID#',
'username' => 'Username',
'email' => 'E-mail Address',
'password' => 'Password',
'state' => 'Account state',
'URL' => 'Your site address',
'rights' => 'Rights Group Membership',
'actions' => 'Actions', );
/**
* The name of the database table for the class.
*
* @var string
*
* @since 1.0
*/
const TABLE_NAME = 'Person';
/**
* The state of the person (account status).
*
* @var \Aplha\Model\Type\Enum
*
* @since 1.0
*/
protected $state;
// ...
/**
* Constructor for the class that populates all of the complex types with default values.
*
* @since 1.0
*/
public function __construct()
{
// ...
$this->username = new SmallText();
$this->username->setRule(Validator::REQUIRED_USERNAME);
$this->username->setSize(70);
$this->username->setHelper('Please provide a name for display on the website (only letters, numbers, and .-_ characters are allowed!).');
$this->email = new SmallText();
$this->email->setRule(Validator::REQUIRED_EMAIL);
$this->email->setSize(70);
$this->email->setHelper('Please provide a valid e-mail address as your username.');
$this->password = new SmallText();
$this->password->setSize(70);
$this->password->setHelper('Please provide a password for logging in.');
$this->password->isPassword(true);
$this->state = new Enum(array('Active', 'Disabled'));
$this->state->setValue('Active');
// ...
}
// ...
}
As you can see in the example above, each attribute of the Alpha\Model\Person record is defined using a data type in the constructor, along with any additional validation rules, sizes, and helper text to display to the user if the validation rules are broken by their input. Each attribute will then be mapped to an appropriate column and data type in the database in the Person table, that will be created for you by Alpha.
Once you have an active record defined and the database in place, carrying out typical CRUD operations becomes trivial:
$record = new Person();
$record->set('email', 'some@user.com');
$record->save();
$record->load(25);
$record->loadByAttribute('email', 'some@user.com');
$records = $record->loadAll(0, 25, 'created_ts'); // load the first 25 Person records sorted by created timestamp
$records = $record->loadAllByAttribute('state', 'Active', 0, 25, 'created_ts');
$record->delete();
For a full list of the supported CRUD and table management methods, check the notes in the Alpha/Model/ActiveRecordProviderInterface.
Alpha supports database transactions via a number of static methods on the ActiveRecord class. Here is a typical example:
try {
ActiveRecord::begin();
$cart = new Cart();
$cart->load(100);
$items = $cart->loadItems();
foreach ($items as $item) {
$item->set('amount', $item->get('ammount') - 1);
$item->save();
}
ActiveRecord::commit();
} catch (AlphaException $e) {
self::$logger->error($e->getMessage());
ActiveRecord::rollback();
// ...
}
Each active record in Alpha maintains a version number that is automatically incremented each time the record is saved. In order to prevent race conditions when two or more seperate threads attempt to save the same record, a version check is performed pre-save and if the version number being saved is older than the version currently in the database, a Alpha\Exception\LockingException is thrown. Naturally in your application, you should capture that exception an try to handle it gracefully:
try {
$record->save();
} catch (LockingException $e) {
// Reload updated record from the database, display something useful to the user and
// then let them try to save the record again...
}
Alpha can optionally maintain the history of each active record for you, by setting the maintainHistory attribute on the record to true. Alpha will then create and maintain a second database table for the record type, suffixed with "_history", where it will save a copy of the record each time it is updated. This means that a full history of changes applied to a record can be maintained indefinetly, if you wish to do so in your application.
$record = new Person();
$record->setMaintainHistory(true);
$record->set('email', 'one@test.com');
$record->save();
$record->set('email', 'two@test.com');
$record->save();
echo $record->getHistoryCount(); // should be 2
// load version 1 of person record 10
$record->load(10, 1);
echo $record->get('email'); // one@test.com
// load version 2 of person record 10
$record->load(10, 2);
echo $record->get('email'); // two@test.com
All controllers in Alpha should inherit from the Controller abstract class and implement the ControllerInterface. The interface defines all of the methods that handle a HTTP request, and includes a method for each of the HTTP verbs (doGET, doPOST, doPUT etc.). Each of these methods accept a Request object as the only parameter, and return a Response object. Here is a simple example:
<?php
namespace My\App\Controller;
use Alpha\Controller\Controller;
use Alpha\Controller\ControllerInterface;
use Alpha\Util\Http\Request;
use Alpha\Util\Http\Response;
class HelloController extends Controller implements ControllerInterface
{
public function doGET($request): \Alpha\Util\Http\Response
{
$name = $request->getParam('name');
return new Response(200, 'Hello '.$name, array('Content-Type' => 'text/plain'));
}
}
Routing a HTTP request to the correct controller is handled by the FrontController. There are two ways to route a request: using a user-friendly URL, or using a secure URL containing an encrypted token that holds the params and controller name for the request.
In your index.php bootstrap file, you should instantiate a new FrontController to handle all requests. Routes can then be added to the FrontController like so:
use Alpha\Controller\Front\FrontController;
use My\App\Controller\HelloController;
// ...
$front = new FrontController();
$this->addRoute('/hello/{name}', function ($request) {
$controller = new HelloController();
return $controller->process($request);
});
$request = new Request(); // this will build the request from super global data.
$response = $front->process($request); // this will map the requested route to the correct controller
echo $response->send(); // send the response to the client
The FrontController::process() method will take care to map the requested route to the correct controller, while the Controller::process() method called within the addRoute closure will map the HTTP request verb (e.g. GET) to the correct controller method (e.g. doGET).
Note that you can also define default request parameters in the route, effectively making them optional:
$this->addRoute('/hello/{name}', function ($request) {
$controller = new HelloController();
return $controller->process($request);
})->value('name', 'unknown'); // if the client requests /hello, return "hello unknown"
If you are concerned about passing sensitive information via the query string or as a route parameter, you can generate a secure URL for the controller like so:
$url = FrontController::generateSecureURL('act=My\App\Controller\HelloController&name=bob');
// $url now something like "http://www.myapp.com/tk/gKwbKqR7uQq-d07z2y8Nnm1JnW_ZTKIUpT-KUJ7pYHxMouGoosktcIUiLKFz4uR8"
Note that the URL generate will be automatically unencoded and decrypted by the FrontController when requested, using the secret encryption key set in your config file during installation.
Views in Alpha are made up of two parts: view classes that are responsible for marshalling active records to views, and view templates that are responsible for defining the actual view content.
A view class should extend the Alpha\View\View class, that defines standard set of methods that are available to a view (createView(), editView(), listView() etc.). You can then override these methods in case you need to do something specific in your view. Typically, there is a one-to-one mapping between a view and the corresponding active record that it is rendering, and the view is responsible for marshalling the record data to an underlying template.
Here is an example where we are injecting in a count of the items in a given shopping cart, to be used later on in the template to display cart details:
<?php
use Alpha\View\View;
use My\App\Model\Cart;
use My\App\Model\Item;
// ...
class CartView extends View
{
// ...
public function detailedView($fields = array()): string
{
// this->record will be set to our cart at this stage during factory instantiation
$items = $this->record->getItems();
$fields['itemCount'] = count($items);
// ...
$html = $this->loadTemplate($this->record, 'detail', $fields);
return $html;
}
}
While you can generate HTML directly in your view class methods and return that, typcially you will want to maintain your HTML in a template where it is easier to manage. The View class provides two seperate methods for loading a template:
// Assuming that this->record is a My\App\Model\Cart instance, this will check in the following locations for the template:
// 1. [app.root]/View/Html/Templates/Cart/detail.phtml (this is where you should place your custom templates)
// 2. [app.root]/Alpha/View/Renderer/Html/Templates/Cart/detail.phtml
$html = $this->loadTemplate($this->record, 'detail', $fields);
// You can also load a fragment template (does not require a record to be passed). It will check for the file in:
// 1. [app.root]/View/Html/Fragments/[fileName]
// 2. [app.root]/Alpha/View/Renderer/Html/Fragments/[fileName]
$html = $this->loadTemplateFragment('html', 'header.phtml', $fields);
In the template itself, you can reference any of the values passed in the $fields array as a regular variable:
<p>There are <?= $itemCount ?> items in your shopping cart.</p>
Alpha provides a number of HTML widgets in the Alpha\View\Widget package, that are very convenient for renderer nice interfaces quickly using Twitter Bootstrap CSS and addition JQuery code.
The following widgets are included:
Widget class | Description | Paramters |
---|---|---|
Alpha\View\Widget\Button | A Bootstrap button. | Some Javascript to execute when the button is pressed. |
Alpha\View\Widget\DateBox | A Bootstrap date picker. | An Alpha\Model\Type\Date or Alpha\Model\Type\Timestamp instance. |
Alpha\View\Widget\Image | A scaled, secure image. | The source path of the original file along with desired dimensions. |
Alpha\View\Widget\RecordSelector | Used for relating records. | A Alpha\Model\Type\Relation instance. |
Alpha\View\Widget\SmallTextBox | One-line text input box. | A Alpha\Model\Type\SmallText instance. |
Alpha\View\Widget\TextBox | Multi-line text input box. | A Alpha\Model\Type\Text instance. |
Alpha\View\Widget\TagCloud | A cloud of popular tags. | The maximum amount of tags to include in the cloud. |
Alpha includes many varied utilities in the Alpha\Util package. The following sections cover some of the highlights.
A data cache is provided, that provides a factory and injectable providers that support Memcache, Redis, and APCu. The classes are provided in the Alpha\Util\Cache package, while the providers implement the Alpha\Util\Cache\CacheProviderInterface. Here is an example using the Redis provider:
use Alpha\Util\Service\ServiceFactory;
// ...
$cache = ServiceFactory::getInstance('Alpha\Util\Cache\CacheProviderRedis','Alpha\Util\Cache\CacheProviderInterface');
$record = $cache->get($cacheKey);
The Alpha\Util\Code\Highlight\HighlightProviderFactory providers objects for converting plain source code into code-highlighted HTML source code. Providers for the Geshi and Luminous libraries are provided. Here is an example:
use Alpha\Util\Service\ServiceFactory;
// ...
$highlighter = ServiceFactory::getInstance('Alpha\Util\Code\Highlight\HighlightProviderGeshi','Alpha\Util\Code\Highlight\HighlightProviderInterface');
$html = $highlighter->highlight($code, 'php');
Alpha provides an email package with an interface for injecting different email providers. Here is an example usage:
use Alpha\Util\Service\ServiceFactory;
use Alpha\Exception\MailNotSentException;
// ...
$mailer = ServiceFactory::getInstance('Alpha\Util\Email\EmailProviderPHP','Alpha\Util\Email\EmailProviderInterface');
try {
$mailer->send('to@mail.com', 'from@mail.com', 'Subject', 'Some HTML...', true);
} catch (MailNotSentException $e) {
// handle error...
}
Alpha can generate an Atom, RSS, or RSS2 feed based on a list of active records, using the classes in the Alpha\Util\Feed package. The FeedController is also provided for convience, that has the following route already set-up in the FrontController:
$this->addRoute('/feed/{ActiveRecordType}/{type}', function ($request) {
$controller = new FeedController();
return $controller->process($request);
})->value('type', 'Atom');
If you want to use the feed classes directly in your application, you can do so:
use Alpha\Util\Feed\Atom;
// ...
$feed = new Atom($ActiveRecordType, $title, $url, $description, $pubDate, $ID);
$feed->setFieldMappings('title', 'URL', 'description', 'created_ts', 'ID');
$feed->addAuthor('Feed author');
$feed->loadRecords(20, 'ID');
$xml = $feed->render();
A validation class is provided for handling typical type checks or for testing for common string patterns (email address, URLs etc.). The class in question is Alpha\Util\Helper\Validator, check the Alpha\Test\Util\Helper\ValidatorTest unit test for lots of code examples on how to use this class.
Alpha provides a framework for injecting optional request filters into the front controller, that are run before any request in routed through there. You can write your own filters that implement the Alpha\Util\Http\Filter\FilterInterface, in addition Alpha provides the following build-in filters that you can choose to enable:
Filter | Purpose |
---|---|
Alpha\Util\Http\Filter\ClientBlacklistFilter | Block any request from a client matching the blacklisted User-Agent. |
Alpha\Util\Http\Filter\IPBlacklistFilter | Block any request from a client matching the blacklisted IP. |
Alpha\Util\Http\Filter\ClientTempBlacklistFilter | Block too many requests from a given client/IP compination for a short period. |
Registering a filter with the front controller is easy, and should be done on application bootstrap, typlically in your index.php file:
use Alpha\Controller\Front\FrontController;
use Alpha\Util\Http\Filter\ClientBlacklistFilter;
use Alpha\Exception\ResourceNotAllowedException;
// ...
try {
$front = new FrontController();
$front->registerFilter(new ClientBlacklistFilter());
} catch (ResourceNotAllowedException $e) {
// the filters will throw this exception when invoked so you will need to handle, e.g. send 403 response.
}
Alpha provides a session abstraction layer to allow the inject of different session providers, which is very useful for example if you want to store your session in an array for unit testing. It is also possible to write providers to store sessions in NoSQL or database backends. Here is an example of using the default PHP session mechanism:
use Alpha\Util\Service\ServiceFactory;
// ...
$session = ServiceFactory::getInstance('Alpha\Util\Http\Session\SessionProviderPHP', 'Alpha\Util\Http\Session\SessionProviderInterface');
$session->set('somekey', 'somevalue'); // you can also pass complex types
echo $session->get('somekey');
Rather than working with PHP super-globals such as _GET and _POST directly, Alpha provides Request and Response classes in the Alpha\Util\Http package to encapsulate those globals.
The attributes of the Request objects (HTTP headers, HTTP verbs, request body etc.) are set during the object instantiation via the __construct($overrides) method, based on the available super-global values. Note that you can override any of the super-global values via the $overrides hash array parameter, which is useful for unit testing.
The Response constructor expects a HTTP response code, body, and optional array of HTTP headers during construction. Here is an example:
use Alpha\Util\Http\Response;
// ...
$response = new Response(200, '<p>hello world!</p>', array('Content-Type' => 'text/html'));
$response->send();
A logging class is provided with a standard set of methods for logging at different levels:
use Alpha\Util\Logging\Logger;
// ...
$logger = new Logger('ClassName');
$logger->debug('Debug information');
$logger->info('Notable information');
$logger->warn('Something went wrong');
$logger->error('A serious error');
$logger->fatal('A fatal error');
$logger->sql('SELECT * FROM...');
$logger->action('Action carried out by the current user logged in the ActionLog table');
The log that is written to is defined by the following configuration properties:
$config->get('app.file.store.dir').'logs/'.$config->get('app.log.file');
Alpha also provides a KPI class, or Key Performance Indicator, that is useful for logging time-sensitive entries that can indicate how your application is performing in production. Here is an example usage:
use Alpha\Util\Logging\KPI;
// ...
$KPI = new KPI('KPIName');
// ...
$KPI->logStep('something happened');
// ...
$KPI->logStep('something else happened');
// ...
$KPI->log();
Each KPI is logged, along with timings and session IDs, in a seperate log file in the following location:
$config->get('app.file.store.dir').'logs/kpi-'.$KPIName.'.csv';
A simple search engine is provided to use in your applications, that is made up of a seach controller and an abstract search API on the backend that searches the main active record database for matching records, based on tags attached to those records.
The key classes are:
Class | Description |
---|---|
Alpha\Controller\SearchController | Handles search queries sent via HTTP requests. |
Alpha\Util\Search\SearchProviderInterface | The main search API. |
Alpha\Util\Search\SearchProviderTags | Implements the SearchProviderInterface using Tag active records. |
Alpha\Model\Tag | Contains a simple string value that helps to describe the related active record. |
Alpha\Controller\TagController | Used to edit tags on a single record, and manage all tags in the admin backend. |
A typical use case is contained in the content management system included with Alpha. When an Article record is saved, a callback is used to generate a set of Tag records based on the content of the Article (excluding stop-words contained in the stopwords-large.ini or stopwords-small.ini files), and those Tag records are related to the Article via a ONE-TO-MANY Relation type, and saved to the main database. Searches are then conducted against the Tag table, and once matching Tags are found we load the related Article records.
For example code, please view the Alpha\Controller\SearchController class and the Alpha\Test\Controller\SearchControllerTest and Alpha\Test\Util\Search\SearchProviderTagsTest unit tests.
Alpha provides a static utility class with two methods for encrypting or decrypting data:
use Alpha\Util\Security\SecurityUtils;
// ...
$encrypted = SecurityUtils::encrypt('Some data');
$decrypted = SecurityUtils::decrypt($encrypted);
The class makes use of the OpenSSL extension in PHP to handle encryption, using the AES 256 algorithm. The secret key used is what you set in the security.encryption.key setting in your config file, which is unique to your application.
It is best practice to build in your own error handling into your application using try/catch blocks, however in the event of an uncaught exception or a generic (non-fatal) PHP error, Alpha provides the Alpha\Util\ErrorHandlers class that should be used in your bootsrap file like so:
set_exception_handler('Alpha\Util\ErrorHandlers::catchException');
set_error_handler('Alpha\Util\ErrorHandlers::catchError', $config->get('php.error.log.level'));
The catchException() method captures an uncaught Exception object and logs it to the application log (message and stacktrace). The catchError() method captures a general PHP error and converts it to an Alpha\Exception\PHPException object, which is then thrown to be captured and logged by catchException().
For bug reports and feature requests, please e-mail: dev@alphaframework.org
On Twitter: @alphaframework