NAV Navbar

Introduction

Welcome to the official documentation for Bellona, an open-source PHP framework inspired by Laravel!

These documents are supposed to introduce many of the key concepts and provide examples showing you how to use Bellona in your own application. However, they do not introduce everything that Bellona has to offer; many other services and functions go undocumented here, but you can find more details at the full Bellona API reference (coming soon...).

Requirements

Installation

Via composer create-project

  1. Create project with composer create-project:
    composer create-project jlwalker/bellona my-project

 Via git clone

  1. Clone the Bellona project directory to quickly setup your project structure:
    git clone https://github.com/jlwalkerlg/bellona.git my-project
    cd my-project
  2. Install all the required packages through composer:
    composer install

Setup

  1. Rename the .env.example file to .env.
  2. Configure Bellona by editing the .env file and the app/config files where appropriate.

That's it!

Development

Docker

If you are using Docker, Bellona comes with a Dockerfile and a docker-compose.yml file for serving your application. Run docker-compose up -d after setup to serve your application to port 80, and Adminer on port 8080. You can seed your database by adding SQL to the database/seed.sql file.

Directory Structure

app

The app directory is where the majority of your development time will be spent, since it contains all of your own controllers, models, views, middleware, libraries, and services.

config

The config contains all of your configuration settings, divided into separate files for each configuration. Note that many of these settings are loaded directly from the .env file in your project root directory, so should be changed there.

If you wish to store your own configuration settings, you are free to do so in the config/index.php file, or use this file to include other configuration files you may wish to create.

public

The public directory contains all of the assets that should be available publicly to the request client (i.e. user's browser), including any JavaScript files, CSS, images, and uploads. Other than the index.php file and the .htaccess file, you are free to arrange these public assets in any way you like.

resources

The resources directory is completely optional but provides a convenient place to place any build assets that have to be bundled before being placed into the public directory.

 routes

The routes directory contains all of the endpoints to which your application can respond.

storage

The storage directory provides a place where you application will automatically store files such as cached HTML output or encryption keys.

.env

The .env.example file in your project root directory contains all of the environment variables which will be loaded by the application. This should be renamed to .env and never commited to a public version control repository, so is listed in the .gitignore file by default.

Dockerfile and docker-compose.yml

These are convenient, pre-built files for Docker users, suitable for development.

.htaccess

The .htaccess file in your project root directory directs all incoming requests into the public directory (where it is further directed to the public/index.php file by the public/.htaccess file).

composer.json

The composer.json file specifies all of your project's dependencies, as well as configuring the vendor autoloader.

Architecture

The Request Lifecycle

Each and every incoming request which hits your app is directed to the public/index.php file by way of .htaccess files.

This file first loads the vendor autoloader, then your environment variables, and then your main configuration settings from your config/index.php file. Next, a number of convenient class aliases are setup for use within your views, before the app itself is booted. An array of Service Provider class names specifies which service providers your application should have access to, and are consequently loaded by the Service Container. At this point, the app has been "booted".

Next, the request itself is handled by the Bellona\Http\Router, which loads your routes and finds the one which matches the incoming request, runs any middleware specified for that route, and finally runs the callback specified for your route. The Bellona\Http\Router also takes care of resolving any services that you have type-hinted in your route middleware or callback from the container and injects them automatically for you.

The Service Container

All of Bellona's core services are registered with the Service Container, which is really just the Bellona\Core\Application itself. Many of these services are deferred, meaning they are registered but not loaded immediately; rather, they are only loaded when required.

You are encouraged to register your own services with the container by writing your own Service Provider or using the App\Providers\AppServiceProvider which comes with Bellona out-of-the-box.

Registering services with the container, as opposed to using classes in the usual way, provides a number of benefits:

Registering your own services.

An example of registering a (deferred) service that can be used to purify dynamic HTML before outputting it:

<?php

namespace App\Providers;

use Bellona\Support\ServiceProvider;

class PurifierServiceProvider extends ServiceProvider
{
    public $defer = true;

    public $services = ['purifier'];

    public function register()
    {
        $this->app->singleton('purifier', function($app) {
            $config = \HTMLPurifier_Config::createDefault();
            return new \HTMLPurifier($config);
        });
    }
}

To register your own service with the container, you would either create your own Service Provider or use the App\Providers\AppServiceProvider which comes with Bellona out-of-the-box.

A Service Provider is a class which extends Bellona\Support\ServiceProvider and specifies a Bellona\Support\ServiceProvider::register() method. This is called automatically by the application when it boots and should register services by way of either the Bellona\Core\Application::bind() method or the Bellona\Core\Application::singleton() method.

Bellona\Core\Application::bind() will ensure that a new instance of your service is created each time you resolve it from the container, whereas Bellona\Core\Application::singleton() will ensure that the same instance is returned each time. You can access the Bellona\Core\Application instance as an inherited property from the Bellona\Support\ServiceProvider.

Both the Bellona\Core\Application::bind() and the Bellona\Core\Application::singleton() methods accept the name of the service you wish to register as the first argument, and a callback function as the second argument. The service container is automatically passed to this callback function so you can resolve any other services on which your own service is dependent, and should return an instance of your service.

If you wish to defer a service, so that it only loads when you require it, simply set a public $defer boolean property and set it as true, as well as a public $services array property which specifies the name of the services your provider provides.

After setting up your service provider (and the service itself), you should list the fully-namespaced name of the service provider class in the $serviceProviders array, within the public/index.php file. This will ensure that the service container knows about your service provider and so can be sure to load it when the application boots on each request.

 Resolving services from the container

<?php

// Get the container instance.
$app = Bellona\Core\Application::getInstance();

// Resolve the 'purifier' service.
$purifier = $app->resolve('purifier');

// Alternatively:
$purifier = $app['purifier'];

// Or:
$purifier = app('purifier');

In order to resolve a service from the container, you will first need a reference to the container instance using $app = Bellona\Core\Application::getInstance(). Then you can either call the $app->resolve($serviceName) instance method, specifying the name of the service you wish to resolve as an argument, or use array notation like so: $app[$serviceName].

Alternatively, Bellona provides a convenience function to more easily resolve services: app($serviceName).

Facades

Below is the source code for the Bellona\Support\Facades\Encrypt facade.

<?php

namespace Bellona\Support\Facades;

use Bellona\Encryption\Encryptor;

class Encrypt extends Facade
{
  protected static $service = Encryptor::class;
}

This allows you to use instance methods of the Bellona\Encryption\Encryptor class as static methods of the corresponding facade:

<?php

$encrypted = Bellona\Support\Facades\Encrypt::encryptString('secretmessage');

This provides a much more readable syntax to the following, which is equivalent:

<?php

$encrypted = app('Bellona\Encryption\Encryptor')->encryptString('secretmessage');

Facades are simply "proxy" classes which defer your static method calls to instance methods of a service registered with the container.

To create a Facade, simply extend the Bellona\Support\Facade class and define a public static property $service whose value is the name of the service you wish to proxy. Then, all static calls to this facade will be automatically redirected to your the corresponding instance methods on service using the dynamic Bellona\Support\Facade::__callStatic() function.

Routing

Defining routes

<?php

// Register a route to listen to GET requests to the /about endpoint,
// and handle the response with an anonymous function:
Router::get('/about', function() {
  echo 'This is the about page.';
});

// Register a route to listen to POST requests to the /about endpoint,
// and handle the response in the about method of the Home controller:
Router::post('/about', 'Home@about');

// PUT, PATCH, and DELETE routes work in the same way:
Router::put('/posts', 'Posts@update');
Router::patch('/posts', 'Posts@edit');
Router::delete('/posts', 'Posts@delete');

// Listen for all HTTP verbs:
Router::all('/users', function() {
  // ...
});

// Listen for multiple specific verbs:
Router::match(['PUT', 'PATCH'], '/users', function() {
  // ...
});

All of your routes should be registered within the routes directory. If you wish to register an API route, do so within the routes/api.php file; otherwise, use the routes/web.php file.

Routes for each verb can be defined by calling the relevent method on the Bellona\Http\Router. Note that an alias has been set up for the Bellona\Support\Facades\Router facade, so that you do not have to use the full namespace within your route files.

The Bellona\Http\Router has a number of methods pertaining to each HTTP verb you wish to register a route for. For example, the Bellona\Http\Router::get() is used to registed a route for GET requests to a specific endpoint and accepts two parameters: a URL defining the endpoint to which the route should be registered, and a callback.

The callback can either be anonymous function to run if the route matches the incoming request, or an action (method) to call on a specific controller in the format 'ControllerName@action'. Bellona will look for your controller inside of the app/controllers directory. If it is nested within a subdirectory thereof, simply specify its namespace relative to the app/controllers directory like so: Path\To\Class.

In addition to a get() method on the Bellona\Http\Router, there is also a post(), put(), patch(), and delete() method which all work in the same way.

If you wish to assign a route to all HTTP verbs, you can use the Bellona\Http\Router::all() method, which accepts the same parameters as Bellona\Http\Router::get().

If you wish to assign a route to multiple HTTP verbs at once (but not all), you can use the Bellona\Http\Router::match() method, which accepts an array of verbs ('GET', 'POST', 'PUT', 'PATCH', or 'DELETE') as the first argument, in addition to the URL and the callback as second and third arguments respectively.

An individual Bellona\Http\Route instance is returned by each of these methods, allowing your to chain on calls to set middleware and custom regex patterns if you wish.

Route parameters

<?php

Router::get('/posts/{id}', function($id) {
  // Display the post with id = $id...
});

Router::get('/posts/author/{id}/page/{page}', function($authorId, $pageNumber) {
  // Display the $pageNumber'th page of all posts by author with id = $authorID...
});

Rather than specifying exact URL endpoints, you can also specify a parameter using curly braces. These parameters will be automatically passed to your callback in the order they are defined. Therefore, the name of your callback arguments which are not type-hinted do not have to match the name of the parameters defined in the URL.

Custom regex

<?php

// Ensure the {id} parameter only matches numbers.
Router::get('/posts/{id}', 'Posts@show')->where('id', '[0-9]+');

// Ensure both the {id} and the {page} parameters only match numbers.
Router::get('/posts/author/{id}/page/{page}', 'Posts@author')->where([
  'id' => '[0-9]+',
  'page' => '[0-9]+'
]);

By default each parameter in your URL is replaced with the simple regex pattern (\w+). If you wish to impose your own regex, for example to ensure an {id} parameter only matches numbers, you can do so with the Bellona\Http\Route::where() instance method.

Dependency injection & implicit model binding

<?php

Router::get('/posts/{post}', function(Bellona\Http\Request $request, App\Models\Post $post) {
  // $request will be the resolved Bellona\Http\Request instance.
  // $post will be the App\Models\Post whose primary key is {post}.
});

The Bellona\Http\Router will attempt to resolve any type-hinted parameters specified in your callback before calling it. If the callback parameter has the same name as one of your URL parameters, it will assume you are expecting a database record and will attempt to find it according to its primary key, where the model it will look for is determined by the name of the type-hinted class, and where the primary key is the value found in the URL. This is implicit model binding. Otherwise, it will assume you wish to resolve a service from the service container and attempt to do so automatically. This is dependency injection.

Middleware

Creating middleware

<?php

namespace App\Middleware;

class Admin
{
  public function run($request)
  {
    // Ensure user is logged in and is an admin user,
    // otherwise redirect...
  }
}

To create middleware, you should create a class in the App\Middleware namespace and define a run() method. This will be called automatically before the callback for your matching route, allowing you to easily implement authentication, authorization, CSRF protection, etc. The Bellona\Http\Request instance will automatically be passed as the first parameter.

Using middleware

<?php

// Specify the 'auth' middleware to run before allowing a user to create a new post.
Router::post('/posts/{post}', 'Posts@create')->middleware('auth');

The auth middleware should be mapped to the name of the corrseponding middleware class in your config/middleware.php array:

<?php

return [
  'auth' => App\Middleware\Authenticate::class,
  // ...
];

You can specify middleware for an individual route with the Bellona\Http\Route::middleware() instance method. This accepts any number of strings of the format name:arg1,arg2..., where name is the name of the middleware specified as a key in your config/middleware.php file, and arg1 and arg2 are any arguments required by your middleware. If no arguments are required, simply exclude them (leaving just name).

If you wish to define middleware for every method in your controller, you can do so using the Bellona\Http\Controller::middleware() method, which works in the same way but applies to all methods in your controller.

Implicit model binding

<?php

Router::get('/posts/{post}', 'Posts@show')->middleware('posts:post');

This will pass the URL parameter typed in by the user and captured in the {post} parameter into your middleware, where you can (optionally) type-hint the appropriate model so that it is passed in automatically:

<?php

class PostsMiddleware
{
  public function run($request, App\Models\Post $post)
  {
    // Decide whether to allow access to the post or not...
  }
}

You can use implicit model binding by passing the name of the URL parameter corresponding to your model as an argument to your middleware and type-hinting the model in the middleware function definition.

Available middleware

CSRF

The App\Middleware\CSRF middleware is called automatically when the incoming request is anything other than a GET request. This will provide protection against CSRF attacks by using the Bellona\Security\CSRF class to require that a valid token by passed either as part of the request body, or as a X-CSRF-TOKEN header.

By default, if a valide token is not found, the middleware will flash an alert message to the session, save the request data, and redirect back to the previous page. You can modify this behaviour by modifying the App\Middleware\CSRF::deny() method.

Authentication

Ensure the user is logged in before allowing them to create a new post:

<?php

Router::post('/posts', 'Posts@create')->middleware('auth');

The App\Middleware\Authenticate middleware uses the Bellona\Auth\Authenticaiton class to require that a user is logged in.

By default, if the user is not logged in, an alert message will be flashed to the session and redirect to the URL_ROOT. You can modify this behaviour by modifying the App\Middleware\Authenticate::deny() method.

Authorization

Ensure a user is authorized to edit a post:

<?php

Router::put('/post/{post}', 'Posts@edit')->middleware('can:edit,post');

The App\Middleware\Authorize middleware requires that the user is logged in and is authorized to perform a given action on a specific model. The name of the action and the model should be passed in as arguments.

To perform this check, App\Middleware\Authorize uses the can() helper, which in turn calls the appropriate method on the appropriate Policy.

Authorization

Authorization is the process by which you ensure that the user has the permisson to perform a given action on a specific database model, for example creating a new post or editing an existing one.

In Bellona, this is done by creating policies.

Creating policies

Create a policy for editing a post:

<?php

namespace App\Policies;

use App\Models\User;
use App\Models\Post;

class PostPolicy
{
  public function edit(User $user, Post $post)
  {
    return $user->id === $post->user_id;
  }
}

Use the policy to authorize user permissions:

<?php

Router::put('/posts/{post}', function(App\Models\Post $post) {
  if (can('edit', $post)) {
    // Logged in user is authorized to edit the post...
  }
});

Alternatively, use the App\Middleware\Authorize middleware:

<?php

Router::put('/posts/{post}', function(App\Models\Post $post) {
  // Logged in user is authorized to edit the post...
})->middleware('can:edit,post');

To create a Policy, create a new class in the App\Policies namespace with the name <Model>Policy where <Model> is the name of the model for which your policies apply. For example, to write any number of policies for the App\Models\Post model, create a App\Policies\PostPolicy class.

Next, create a public method to authorize a specific action. This method should return true only if the user is authorized; otherwise, return false.

Then, use the Bellona\Auth\Authorization::can() method, or the can() helper to check whether or not the user is authorized to perform said action.

Bellona\Auth\Authorization::can() accepts three arguments: $action, $model, and $user. $action is the name of the function to call in your Policy class. $model is either the model instance for which to authorize permissions, or, if a specific instance is not required, the name of the model's class name as a string (e.g. for creating a new App\Models\Post). $user is optional and is the specific user to authorize. If no $user is given, the currently logged in user will be used instead.

Controllers

Controllers are classes which handle incoming requests, usually house much of the necessary logic, communicate with a Model to fetch or update data, and return a response in the form of a View.

Creating controllers

Define a controller and an action to display all posts:

<?php

namespace App\Controllers;

use Bellona\Http\Controller;

class Posts extends Controller
{
  public function show()
  {
    // Display all posts...
  }
}

Register the controller callback with a route:

<?php

Router::get('/posts', 'Posts@show');

To create a controller, simply write a new class in the App\Controllers namespace and extend the Bellona\Http\Controller. You can then define methods to respond to the incoming request and register a route to call one of your controller's methods when it matches the incoming URL.

Extending Bellona\Http\Controller gives you access to the Bellona\Http\Controller::middleware() method, which works the same as the Bellona\Http\Route::middleware() method, except that the middleware is defined for all methods within your controller.

Models

Bellona uses the active record pattern, wherein models are classes whose instances represent a single record in a database. Each column in the database for that record maps to a property on the model instance. The model instance can also have methods which interact with or provide information about the corrseponding database record.

Creating models

<?php

namespace App\Models;

use Bellona\Database\Model;

class Post extends Model
{
  protected $table = 'posts';
  protected $fillable = ['author_id', 'title', 'body'];
}

To create a model, write a new class in the App\Models namespace which extends Bellona\Database\Model. Each model requires a protected $table property, which is the name of the table in your database for which your model represents records. Additionally, each model requires a protected $fillable property if it is to be used to insert/update database records. This should be an array of column names in your database table, listing only the columns which you want to allow your model to update, either with new records or as updates to existing ones. This provides some security against updating a column which you did not intend, either accidentally or through a malicious user.

<?php

namespace App\Models;

use Bellona\Database\Model;

class Post extends Model
{
  protected $table = 'posts';
  protected $fillable = ['author_id', 'title', 'body'];
  protected $primaryKey = 'post_id';
}

Using models

<?php

// Save a new post to the database
$post = new App\Models\Post;
$post->author_id = 1;
$post->title = 'Post Title.';
$post->body = 'Post body...';
$post->save();

// Update an existing post.
$post->body = 'Updated post body...';
$post->save();

// Delete an existing post.
$post->delete();

To save a model instance to the database, use the Bellona\Database\Model::save() instance method. If the model has a property corresponding to the relevant table's primary key, Bellona will update the associated record in the database with all your model instance's properties that are listed in the $fillable array. If the model does not have a primary key, it will create a new record in the database and assign the inserted primary key to your model instance.

To delete a record associated with a model instance, use the Bellona\Database\Model::delete() instance method on your model instance.

You can also use the Bellona\Database\Model::assign() method to assign multiple values to properties on your model instance at once.

<?php

use App\Models\Post;

// Find the post with primary key = 1
$post = Post::find(1);

// Find the posts with primary key = 1 or 2 or 3
$post = Post::find([1, 2, 3]);

// Insert a new post
$post = Post::create(['author_id' => 1, title => 'Post Title.', body => 'Post body...']);

// Insert multiple new posts
$post = Post::create([
  ['author_id' => 1, title => 'Post Title.', body => 'Post body...'],
  ['author_id' => 1, title => 'Post Title 2.', body => 'Post body...'],
  ['author_id' => 2, title => 'Post Title 3.', body => 'Post body...'],
]);

// Delete a single post
Post::delete(1);

// Delete multiple posts
Post::delete([1, 2, 3]);

Extending Bellona\Database\Model will provide three additional methods which can be called statically: Bellona\Database\Model::find(), Bellona\Database\Model::create(), and Bellona\Database\Model::destroy().

<?php

// Get all posts
$posts = App\Models\Post::get();

// Get 5 posts where author_id = 1
$posts = App\Models\Post::where('id', 1)->limit(5)->get();

Additionally, extending Bellona\Database\Model gives you access to all of the Bellona\Database\QueryBuilder instance methods as static methods on your Model class. By default, any records returned using these methods will return instances of your Model.

Views

Views are files which hold your presentational markup.

Creating views

To create a view, simply write a new file in the app/views directory.

Using views

<?php

use Bellona\Support\Facades\View;

$view = app('Bellona\View\ViewFactory')->make('home');
$view->render();

// Alternatively...
View::make('home');

Pass data to a view:

<?php

$view = app('Bellona\View\ViewFactory')->make('home');
$view->render(['title' => 'Welcome!']);

// This will make a $title variable available in app/views/home.php, which holds the value 'Welcome!'.

To get access to the view as an instance of Bellona\View\View, use the Bellona\View\ViewFactory::make() instance method, which takes in the path to your view file, relative to the app/views directory and excluding the .php extension, as an argument. You can use the Bellona\Support\Facades\View facade to make this cleaner.

You can then render your view, thereby sending it as HTML output to the user's browser, by using the Bellona\View\View::render() method. This takes in an optional array of data in the form of key-value pairs to pass to the view, so that each key is available as a variable within your view file.

Alternatively, you can use the global render() helper, which accepts the path to your view as the first argument, and the optional data array as the second argument.

Sharing data between views

<?php

use Bellona\Support\Facades\View;

// Share the $title variable between all views hereafter.
View::share(['title' => 'Welcome!']);

// Render the app/views/home.php view, which will have access to the $title variable.
render('home');

// Render the app/views/about.php view, and override the $title variable for this view only.
render('about', ['title' => 'About Us']);

To share data between views, use the Bellona\View\ViewFactory::share() method, which takes in the data you wish to share as an argument. All view instanced created after this call will then have access to this shared data, in addition to any specific data passed only to a single view.

Extending templates

Define an app/views/templates/default.php template:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><?= (isset($title) ? h($title) . ' | ' : '') . SITE_NAME ?></title>
</head>

<body>

  <!-- Main content -->
  <?php $this->yield('body') ?>

  <!-- Footer -->
  <?php $this->yield('foot') ?>

</body>

</html>

Extend your template from a view and inject content into the default body yield:

<?php $this->extends('default') ?>

<p>This content will be injected into the 'body' yield.</p>

Inject content into the body and the footer yields:

<?php $this->extends('default') ?>

<?php $this->block('body') ?>
  <p>This content will be injected into the 'body' yield.</p>
<?php $this->endblock() ?>

<?php $this->block('foot') ?>
  <p> This content will be injected into the 'foot' yield.</p>
<?php $this->endblock() ?>

Bellona allows you use files containing markup as templates which can be extended by your views.

First, write a template file in the app/views/layouts directory. This file should call the Bellona\View\View::yield() instance method anywhere you wish to inject content from your views which extend this template. The Bellona\View\View::yield() takes in the name of your content as an argument. If the content does not exist, an empty string will be injected instead.

Next, write a view file which extends the template using the Bellona\View\View::extends() method, which takes in the path to the template to extend, relative to the app/views/layouts directory and excluding the .php extension, as an argument.

You can define the content you wish to inject within your views by preceding your content with a Bellona\View\View::block() call, and ending it with a Bellona\View\View::endblock() call. Bellona\View\View::block() takes in the name to assign to your content, which is then used by the Bellona\View\View::yield() method in your template.

If you do not specify any content blocks, your view will be automatically injected into the 'body' yield, if your template defines one. If you do not specify any content blocks and your template does not define a 'body' yield, nothing will be output. If you do define a content block, the 'body' yield will not be injected automatically, but should be injected using a content block as usual.

CSRF Protection

Bellona comes with the CSRF middleware enabled by default. All incoming requests that are not GET requests are required to present a CSRF token either in the request body or in an X-CSRF-TOKEN header. If a valid token is not presented with the request, the middleware will flash a session message and redirect back to the previous page before exiting the script, though you can change this behaviour by modifying the App\Middleware\CSRF::deny() method.

CSRF tokens in views

<form action="/posts" method="POST">
  <?= CSRF::input() ?>
  ...
</form>

To include a valid CSRF in your view, thereby ensuring it gets sent with a form, Bellona provides a Bellona\Security\CSRF::token() instance method which returns the valid token as a string. In your view, you should add this as the value of a hidden input field in your form with the name of csrf_token. Alternatively, this can be done automatically by using the Bellona\Security\CSRF::input() instance method directly in your form.

CSRF tokens with ajax

Output the CSRF token in your view, somewhere it can be retrieved client-side with JavaScript:

  <meta name="csrf-token" content="<?= CSRF::token() ?>"

Send the CSRF token with the request, either in the request body or as an X-CSRF-TOKEN header, and capture the new CSRF from the response for use in further requests:

fetch("/api/posts", {
  method: "POST",
  headers: {
    "Content-type": "application/json",
    "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  },
  body: JSON.stringify({
    title: "The post title",
    body: "The post body"
  })
}).then(res => {
  const newCsrfToken = res.headers.get("XSRF-TOKEN");
  document.querySelector('meta[name="csrf-token"]').setAttribute('content', newCsrfToken);
  ...
});

Bellona sends a new CSRF token back with each response in the form of a XSRF-TOKEN response header. If you are using ajax to send requests, you should capture this token and return it with any subsequent request that is not a GET request, otherwise the request will be denied.

Form Validation

The Bellona\Validation\Validator library can be used to validate incoming requests.

 Manually validating data

<?php

$data = ['email' => 'invalidemail'];

$rules = ['email' => 'required|format:email'];

$validator = new Bellona\Validation\Validator($data, $rules);

$validator->run();

if ($validator->validates()) {
  // All validations passed...
} else {
  // Get the validation errors.
  $errors = $validator->getErrors();
}

To validate data manually, you must instantiate the Bellona\Validation\Validator class and pass in any data you wish to validate yourself.

Bellona\Validation\Validator::__construct() takes in the data you wish to validate as the first argument, as well as an array of validation rules to run against said data as the second argument. You can then run the validations with the Bellona\Validation\Validator::run() method and subsequently check that all validations passed with the Bellona\Validation\Validator::validates() method. If all validations passed, this will return true; otherwise, it will return false and the validation errors will be captured and retrievable with the Bellona\Validation\Validator::getErrors() method.

Validating request data

<?php

Router::post('/posts', function(Bellona\Http\Request $request) {
  $request->validate([
    'email' => 'required|format:email'
  ]);

  // All validations passed...
});

The Bellona\Http\Request instance comes with a Bellona\Http\Request::validate() function to automatically validate the incoming request data for you. This only takes in the validation rules as an argument, and runs them against the incoming request body automatically.

If any validations fail, they will be automatically flashed to the session as an errors array and available from your view as an $errors variable. The user will also be redirected back to the previous location and the script will be exited.

File Uploads

The Bellona\Uploads\FileUpload class can be used to securely verify and store files uploaded by the user in the request.

 Uploading files

<?php

$file = $_FILES['image'];

$upload = new Bellona\Uploads\FileUpload($file);

if ($upload->store(PUBLIC_ROOT . '/uploads') {
  // File was successfully uploaded...
  $uploadedName = $upload->getName();
} else {
  // Get the error which caused the failure.
  $error = $upload->getError();
}

Bellona\Uploads\FileUpload::__construct() takes the specific array within the $_FILES superglobal corresponding to the file you wish to upload as an argument. You can then store the file permanently with the Bellona\Uploads\FileUpload::store() method, which takes in the absolute path to the directory where you want to store the file as an argument. If the file was successfully stored, this will return true; otherwise it will return false and the error which caused the failure can be retrieved with the Bellona\Uploads\FileUpload::getError() method.

The Bellona\Uploads\FileUpload class has some default behaviours you should be aware of.

You can override most of these defaults as well as setting other options with the Bellona\Uploads\FileUpload::setOptions() array, which takes in an array of options you wish to set.

If you wish to retrieve the name with which a file was uplaoded, you can use the Bellona\Uploads\FileUpload::getName() instance method.

Deleting a file

<?php

$upload->delete();

If you wish to delete a file after storing it, for example if something goes wrong later in your script, you can use the Bellona\Uploads\FileUpload::delete() method, which returns true if the file was successfully delete; otherwise it will return false.

Uploading multiple files

<?php

$profilePic = $_FILES['profile_pic'];
$coverImg = $_FILES['cover_img'];

$files = ['profilePic' => $profilePic, 'coverImg' => $coverImg]
$destination = PUBLIC_ROOT . '/uploads';

$result = Bellona\Uploads\FileUpload::upload($files, $destination);

if ($result) {
  // All files were successfully uploaded...
  $profilePicName = Bellona\Uploads\FileUpload::getUploadedName('profilePic');
  $coverImgName = Bellona\Uploads\FileUpload::getUploadedName('coverImg');
} else {
  // Something went wrong...
}

The Bellona\Uploads\FileUpload class provides a Bellona\Uploads\FileUpload::upload() static method for uploading multiple files at once. It takes an array of files to upload as the first argument and a destination directory as a second argument. Optionally, you can also pass a third argument specifying the options to set for all files. It will return true on success and false on failure.

You can then get the name with which any of the files were uploaded using the Bellona\Uploads\FileUpload::getUploadedName() static method and passing it the key corresponding to the file whose name youi wish to retrieve.

If something goes wrong while uploading one of the files, all the files that were already uploaded will be deleted automatically.

Query Builder

The Bellona\Database\QueryBuilder is based on Laravel's query builder and provides a number of methods to fluently create database queries.

Fetching multiple records with DB::get()

<?php

$posts = DB::table('posts')->get(); // fetch all posts
$posts = DB::table('posts')->where('user_id', 10)->get(); // fetch all posts where user_id = 10
$posts = DB::table('posts')->where('user_id', '!=', 10)->get(); // fetch all posts where user_id != 10
$posts = DB::table('posts')->limit(20)->offset(50)->get(); // fetch 20 posts, starting from the 51st
$posts = DB::table('posts')->where('user_id', 10)->limit(10)->offset(10)->get(); // fetch 10 posts where user_id = 10, starting from the 11th

All results are returned as an array of object instances of the stdClass class.

Fetching a single record with DB::first()

<?php

$posts = DB::table('posts')->first(); // fetch first post

This adds a LIMIT 1 to the query and returns the record directly, rather than an array.

Counting records with DB::count()

<?php

$posts = DB::table('posts')->count(); // count all posts

This returns and integer of the number of records that match the query.

Fetching records with DB::join(), DB::leftJoin(), DB::rightJoin(), and DB::select()

<?php

$posts = DB::table('posts')->join('users', 'posts.user_id', '=', 'users.id')->get(); // inner join posts table with users table on posts.user_id = users.id and retrieve all records
$posts = DB::table('posts')->leftJoin('users', 'posts.user_id', '=', 'users.id')->get(); // left join posts table with users table on posts.user_id = users.id and retrieve all records
$posts = DB::table('posts')->rightJoin('users', 'posts.user_id', '=', 'users.id')->get(); // right join posts table with users table on posts.user_id = users.id and retrieve all records
$posts = DB::table('posts')->join('users', 'posts.user_id', '=', 'users.id')->select('posts.*', 'users.id as userId')->get(); // inner join posts table with users table on posts.user_id = users.id and retrieve all records, returning only the id from the users table

Inserting records

<?php

$key = DB::table('posts')->insert(['title' => 'Post Title', 'body' => 'Here goes the post body...']);
$key = DB::table('posts')->insert([
    ['title' => 'Post Title', 'body' => 'Here goes the post body...'],
    ['title' => 'Post Title #2', 'body' => 'Here goes the second post body...']
]);

If at least one record is successfully inserted into the database, DB::insert() returns the primary key of the last inserted record. If no records are inserted, it returns false.

Updating records

<?php

$result = DB::table('posts')->update(['title' => 'Post Title']); // update all posts
$result = DB::table('posts')->where('id', 10)->update(['title' => 'Post Title']); // update only posts where id = 10

Returns the number of records effected by the query.

Deleting records

<?php

$result = DB::table('posts')->delete(); // delete all posts
$result = DB::table('posts')->where('id', 10)->delete(); // delete posts where id = 10

Returns the number of records effected by the query.

Direct queries with DB::query()

This method accepts an SQL query string and an optional array of parameters to bind before executing.

<?php

$posts = DB::query('SELECT * FROM posts WHERE user_id = ?', [10]);
$posts = DB::query('SELECT * FROM posts WHERE user_id = :user_id', [':user_id' => 10]);