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
- PHP >= 7.1
- mod_rewrite Apache module enabled
- PDO PHP extension
- OpenSSL PHP extension
- JSON PHP extension
Installation
Via composer create-project
- Create project with
composer create-project
:
composer create-project jlwalker/bellona my-project
Via git clone
- Clone the Bellona project directory to quickly setup your project structure:
git clone https://github.com/jlwalkerlg/bellona.git my-project
cd my-project
- Install all the required packages through composer:
composer install
Setup
- Rename the
.env.example
file to.env
. - Configure Bellona by editing the
.env
file and theapp/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.
app/controllers
contains all of your controller classes. As with all of your class files, each file should be named the same as the the class it contains, starting with an uppercase letter. E.g.Home.php
would contain theHome
controller. You may also nest your controllers into subdirectories within theapp/controllers
directory, so long as your routes reflect this nesting.app/models
contains all of your models. They should be named in the same fashion as yourapp/controllers
files and can be similarly nested within subdirectories.app/views
contains all of your views.app/middleware
contains all of your middleware.app/policies
contains all of your authorization policies.app/providers
provides a convenient place to store your service providers. However, you may place your service providers wherever you want, as long as you list them appropriately in thepublic/index.php
file.app/facades
contains all of your facades, though you may choose to put these wherever you like.app/lib
contains any libraries you might wish to create, though you are free to create these any place you desire.app/helpers
contains any helper classes/functoins you might wish to create, though you can place these wherever you like.
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.
config/index.php
contains the main configuration settings for your site, including file and URL paths.config/session.php
contains configuration settings for user sessions, including which driver you wish to use.config/auth.php
contains configuration settings for authentication, including which driver you wish to use.config/middleware.php
contains an array of middleware you wish make available to your application, where the keys are the names you wish to assign and the values are the fully-namespaced class names of the middleware classes themselves.
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.
public/index.php
is the main PHP file through which all of the incoming requests to your site are directed, and bootstraps your entire application.public/.htaccess
rewrites the incoming request URL so that all requests, other than those for existing files (e.g. public images), are directed to thepublic/index.php
file.
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.
routes/web.php
contains all of your regular endpoints to which your application responds with regular HTML output.routes/api.php
contains all of your API endpoints. These are automatically prefixed with/api
when they are loaded by theBellona/Http/Router
.
storage
The storage
directory provides a place where you application will automatically store files such as cached HTML output or encryption keys.
storage/cache
contains all your cached HTML output.
.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:
- You can configure service providers such that the dependencies of your services are automatically taken care of by the service container.
- You can easily register singletons with the container so that the same instance is supplied each time you resolve a service from the container.
- Your services can be automatically injected into your controller methods as well as your middleware (dependency injection).
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 yourconfig/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.
- The maximum size allowed for a file is the same as
upload_max_filesize
in yourphp.ini
file. - If the destination directory does not exist, it will be created.
- The file will be renamed by appending a number to the end of the file name if a file with the same name already exists in the destination directory.
- Only the following extensions are permitted: jpg, jpeg, gif, png, svg, webp.
- Any whitespace in the file name will be replaced with underscores.
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]);