Requirements

  • PHP >= 8.5.0
  • Composer
  • MySQL/MariaDB
  • Docker (optional)
 

Installation

  1. Create new Beacon project:
    composer create-project shworx/beacon my-project
  2. Enter the project directory:
    cd my-project
  3. Install dependencies:
    composer install
  4. Copy the environment configuration:
    cp .env.example .env
  5. Edit environment configuration as you need it
  6. Generate an application secret:
    php console app:secret
  7. Copy the generated application secret and add it as value to the .env file variable APP_SECRET=
  8. Run database migrations:
    php console migrate
That's all, you're now ready to start building your PHP web application.
To the Top  

Project structure

After you're done with the installation, you should have the following project structure.


project-directory
├── app
│    ├── Console
│    │    └── Commands      // Contains all the Console commands
│    ├── Container
│    ├── Controllers        // Contains all Controllers
│    ├── Database
│    ├── DTO                // Contains all the DTO's
│    ├── Enums
│    ├── Exceptions
│    ├── Helpers
│    ├── Http
│    ├── Interfaces
│    ├── Middleware
│    ├── Models             // Contains all Models
│    ├── Providers
│    ├── Routing
│    ├── Services
│    ├── Support
│    └── View
├── bootstrap
├── config
├── console
├── database
│    └── migrations
├── docker
├── public
│    ├── assets
│    │    ├── css
│    │    ├── js
│    │    ├── favicons
│    │    └── img
├── resources
│    ├── stubs
│    └── views              // Contains all the Twig templates
├── routes
│    └── web.php            // This is where the routes are defined
├── storage
└── tests
To the Top  

Routing

Basic routes

Beacon currently supports 2 routing methods: get() and post().

Both route methods requires at least 2 parameters: Path and Callback.


Below are examples of both routing methods.


// File: routes/web.php

// GET route
$router->get('/', [App\Controllers\HomeController::class, 'index']);

// POST route
$router->post('/my-path', [App\Controllers\MyController::class, 'myMethod']);

Named routes

Beacon also supports named routes. To define a name for a route, just add a third parameter to the route methods, with the name of the route.


// File: routes/web.php

// Named GET route
$router->get('/', [App\Controllers\HomeController::class, 'index'], 'home');

// Named POST route
$router->post('/my-path', [App\Controllers\MyController::class, 'myMethod', 'my-route']);

Routes with parameters

You can also define routes with one or more parameters.


// File: routes/web.php

// GET route with parameters
$router->get('/my-path/{param_one}/{param_2}', [App\Controllers\MyController::class, 'myMethod'], 'my-route');

Due to dependency and parameter injection, retrieving route parameters in controllers is easy:


// File: app/Controllers/MyController.php
use App\Controllers\Controller;
use App\Http\Response;

class MyController extends Controller
{
    public function myMethod($param_one, $param_2): Response
    {
        // Your code
    }
}

To retrieve POST parameters in controllers, you need to pass the Request object to the controller method first. The Request object provides then methods which allows you to retrieve POST and GET parameters (or all).


// File: app/Controllers/MyController.php
use App\Controllers\Controller;
use App\Http\Request;
use App\Http\Response;

class MyController extends Controller
{
    public function myMethod(Request $request): Response
    {
        // Retrieve the value of the POST field "name"
        $name = $request->post('name');

        // Retrieve all request parameters
        $all = $request->all();

        // Retrieve a URI query parameter (GET parameter)
        $param_1 = $request->query('param_1');
    }
}

Route groups

You can group routes together with the route method group().


Routing group without prefix:


// File: routes/web.php

$router->group(
    prefix: '',
    callback: function (Router $router) {
        $router->get('/', [App\Controllers\HomeController::class, 'index'], 'home');
        $router->get('/about', [App\Controllers\HomeController::class, 'about'], 'about');
    }
);

Alternatively you can also add a "prefix" to the group. If a group is defined with a prefix, then the prefix will be at the beginning of each route in this group.


Routing group with prefix:


// File: routes/web.php

$router->group(
    prefix: 'admin',
    callback: function (Router $router) {
        $router->get('/dashboard', [App\Controllers\Admin\DashboardController::class, 'index'], 'admin-dashboard');
        $router->get('/users', [App\Controllers\Admin\UsersController::class, 'index'], 'admin-users');
    }
);

// The routes will then be:
// /admin/dashboard
// /admin/users

Route middleware

To assign a middleware to all routes within a group, you need to pass the argument middleware to the route method group().


In the example below, we're assigning the Auth middleware to the route group "admin".
The Auth middleware makes sure, that all routes within this group are only accessible if the User is authenticated. You can assign multiple middlewares to a route group.


// File: routes/web.php

$router->group(
    prefix: 'admin',
    callback: function (Router $router) {
        $router->get('/dashboard', [App\Controllers\Admin\DashboardController::class, 'index'], 'admin-dashboard');
        $router->get('/users', [App\Controllers\Admin\UsersController::class, 'index'], 'admin-users');
    },
    middleware: [AuthMiddleware::class]
);
To the Top  

Controller

Controllers are the place where you handle all the request logic from your defined routes.

Creating controller

In Beacon, you can create controllers manually, or via console.

To create a new controller via console just do it like below.

console make:controller MyController

This will create the new controller file app/Controllers/MyController.php

You can also create a new controller in a specific namespace. To do so, just add the namespace in front of the controller name.

console make:controller MyNamespace/MyController

This will create the new controller file app/Controllers/MyNamespace/MyController.php

Dependency injection in controller methods

As you may have seen in the section 4.3 Routes with parameters, Beacon provides not only parameter injection, but also dependency injection.

Dependencies are objects like:
  • Request
  • ValidationService
  • AuthService
  • Flash
  • MailService

Injecting a dependency into a controller method is as simple as described in the section 4.3 Routes with parameters:


use App\Controllers\Controller;
use App\Http\Request;

class MyController extends Controller
{
    public function myMethod(Request $request): Response
    {
        // Your code logic
    }
}

You can also inject multiple dependencies and route parameter at once. Important: Dependencies comes before route parameters.


use App\Controllers\Controller;
use App\Http\Request;

class MyController extends Controller
{
    public function myMethod(
        Request $request,
        ValidationService $validator,
        MailService $mailer,
        $param_first,
        $param_second
    ): Response {
        // Your code logic
    }
}

Validation

Beacon integrates Symfony Validator with DTO (Data Transfer Object) based validation.

The first step requires the creation of a DTO object:

console make:dto ExampleDto

This will create the file: app/DTO/ExampleDto.php

The next step is defining the validation assertions in the DTO as so-called "attributes", and adding the properties to the existing fromArray() and toArray() methods.


// File: app/DTO/ExampleDto.php

namespace App\DTO;

use Symfony\Component\Validator\Constraints as Assert;

class ExampleDto
{
    public function __construct(
        #[Assert\NotBlank]
        #[Assert\Email]
        public string $email,

        #[Assert\NotBlank]
        #[Assert\Length(min: 4, max: 100)]
        public string $name,
    ) { }

    public static function fromArray(array $data): self
    {
        return new self(
            email: trim($data['email']) ?? '',
            name: trim($data['name']) ?? '',
        );
    }

    public function toArray(): array
    {
        return [
            'email' => $this->email,
            'name' => $this->name,
        ];
    }
}

Now we can use this DTO to validate our form fields in our controller.


// File: app/Controllers/MyController.php

use App\Controllers\Controller;
use App\DTO\ExampleDto;
use App\Exceptions\ValidationException;
use App\Http\Request;
use App\Services\ValidationService;

class MyController extends Controller
{
    /**
     * @param Request $request
     * @param ValidationService $validator
     * @param MailService $mailer
     *
     * @throws ValidationException
     * @return Response
     */
    public function myMethod(
        Request $request,
        ValidationService $validator,
    ): Response {
        $dto = ExampleDto::fromArray($request->all());
        $validator->validate($dto);

        // Your additional code logic
    }
}

If the validation is successful, the code after

$validator->validate($dto);
will be executed. In case the validation fails, an ValidationException is thrown, which contains the errors incl. the related form field names, and is then handled by the Kernel.

To display the validation errors in the front-end (Twig template), please refer to the section 6.2 Handle validation errors.

To know more about the available validation constraints (attributes), please refer to the official Symfony Validation documentation.

Mailing

Beacon uses internally the PHPMailer to send emails. Also, you need an existing email account, which allows you to send emails via SMTP.
The configuration for PHPMailer have to be made in the .env file under "MAILER SETTINGS".

To send emails from within a controller, it requires only a few lines of code.

In this example, we use the previously created DTO and just inject the additional required MailService object in our method.


// File: app/Controllers/MyController.php

use App\Controllers\Controller;
use App\DTO\ExampleDto;
use App\Exceptions\ValidationException;
use App\Http\Request;
use App\Services\MailService;
use App\Services\ValidationService;

class MyController extends Controller
{
    /**
     * @param Request $request
     * @param ValidationService $validator
     * @param MailService $mailer
     *
     * @throws MailerException
     * @throws ValidationException
     * @return Response
     */
    public function myMethod(
        Request $request,
        ValidationService $validator,
        MailService $mailer,
    ): Response {
        $dto = ExampleDto::fromArray($request->all());
        $validator->validate($dto);

        // Sending a HTML message by using a Twig template
        $message = $this->view->render('mail/my_mail.twig', [
            'name' => $name,
        ]);

        $mailer->send(
            $dto->email,
            'Your Subject',
            $message,
            true,
        );

        // Alternative you can also send a PLAIN text message. To do so, you need to pass (bool) false as fourth
        // parameter to the send() method
        $mailer->send(
            $dto->email,
            'Your Subject',
            sprintf('Hallo .', $name),
            false,
        );
    }
}
To the Top  

Twig

Beacon uses the Twig template engine (v3.x). Therefore, we won't go into the template basics here, and refer to the official Twig documentation.
Instead, we will document here only the Beacon specific Twig functions and validation error handling.

Handle validation errors

The errors thrown by the ValidationService and handled in the Kernel, can be retrieved in Twig templates via the global errors variable. This variable is an array, which contains the form field name, for which the error is thrown, and also the related error messages.

The best way to display these errors is, to first check if the related field name exists in errors, and then walk through the field errors (which is also an array) to display them.

Example (show all errors for a specific field):


<form>
    <input
        type="email"
        name="email"
        class="input"
        placeholder="[email protected]"
    >
    <!-- Check if errors for the "email" field exists -->
    {% if errors.email is defined %}
        <div class="form-error mt-xs">
            <!-- Walk through the email field errors and display them -->
            {% for error in errors.email %}
                {{ error }}<br>
            {% endfor %}
        </div>
    {% endif %}
</form>

Example (show only the first error for a specific field):


<form>
    <input
        type="email"
        name="email"
        class="input"
        placeholder="[email protected]"
    >
    <!-- Check if errors for the "email" field exists -->
    {% if errors.email is defined %}
        <div class="form-error mt-xs">
            <!-- Show only the first error for this form field -->
            {{ errors.email|first }}
        </div>
    {% endif %}
</form>

The errors exists check, can also be used to add CSS class to the form field, to highlight the form field additionally.

Example:


<form>
    <input
        type="email"
        name="email"
        class="input{% if errors.email is defined %} field-error{% endif %}"
        placeholder="[email protected]"
    >
    {% if errors.email is defined %}
        <div class="form-error mt-xs">
            {% for error in errors.email %}
                {{ errors }}<br>
            {% endfor %}
        </div>
    {% endif %}
</form>

If a validation fails, Twig provides over another global variable old the old values of successfully validated form fields, which can be reused as "placeholder".

Example:


<form>
    <input
        type="email"
        name="email"
        class="input"
        placeholder="{{ old.email|default('[email protected]')|e }}"
    >
    {% if errors.email is defined %}
        <div class="form-error mt-xs">
            {{ errors.email }}
        </div>
    {% endif %}
</form>

Very important note:
The elements in the global errors variable are internally retrieved from a "flash" message, and therefore not persistent. This means as soon as you retrieve the element, it will be removed from the global errors variable. This is why you need to check the existence of the element first via is defined.

Helper functions

Beacon provides a few Twig helper functions, which can be used in Twig templates.

  • route(): Get the route by name
    Example (without query arguments):
    
    <a href="{{ route('home') }}">Home</a>
    Result: <a href="/">Home</a>
    
    Example (with query arguments):
    
    <a href="{{ route('my-route', {'param_first' : 'first+value', 'param_second' : 'second value'}) }}">My route with params</a>
    Result: <a href="/my-path/first%2Bvalue/second+value">My route with params</a>
    

    Note: As you can see, the passed values are automatically URL encoded.

  • old(): Retrieve all "old" flash data as array. Note: The related flash data will not be removed.
  • errors(): Retrieve all "errors" flash data as array. Note: The related flash data will not be removed.
  • flash(): Retrieves the flash data for "key". Note: The related flash data will be removed. Example usage:
    {{ flash('key-name', 'default value') }}
  • guest(): Returns true if current user is not authenticated, otherwise false.
  • currentRoute(): Returns the current route, example: /my-path
  • isActiveRoute(): Returns true if the passed route name is the current active route, otherwise false. Example usage:
    
    {% if isActiveRoute('my-route') %}
        "my-route" is the current active route.
    {% else %}
        "my-route" is NOT the current active route
    {% endif %}
    
  • csrf(): Returns an HTML hidden form element with an CSRF token as value. Example:
    
    {{ csrf() }}
    Result: <input type="hidden" name="_csrf" value="0d924a123d841aa1908a1599a87e5ab02a73703b2feb5a4229961d316f7a1c07">
    
  • asset(): Returns the "assets" URL path. Example:
    
    <link rel="stylesheet" href="{{ asset('css/styles.css') }}">
    Result: <link rel="stylesheet" href="/assets/css/styles.css">
    
  • app_url(): Returns the full application URL incl. path. Example:
    
    <link rel="stylesheet" href="{{ app_url('css/styles.css') }}">
    Result: <link rel="stylesheet" href="https://example.org/assets/css/styles.css">
    

Global variables

Beacon provides a few global variables as listed below.

  • old: Same functionality as the Twig helper function old()
  • errors: Same functionality as the Twig helper function errors().
  • _appName: Contains the value of the .env variable APP_NAME.
  • _appSlogan: Contains the value of the .env variable APP_SLOGAN.
  • _appVersion: Contains the value of the .env variable APP_VERSION.
  • _appCopyright: Contains the value of the .env variable APP_COPYRIGHT.
To the Top  

Application helper functions

Beacon not only provides Twig helper functions, but also application helper functions, which can be used anywhere in application PHP files.

  • base_path(): Returns the application base path
  • app_path(): Returns a app/ path. Examples:
    
    $path = app_path();
    // $path > /var/www/example.org/my_app
    $path = app_path('my-path');
    // $path > /var/www/example.org/my_app/my-path/
    $path = app_path('my-path/example.txt');
    // $path > /var/www/example.org/my_app/my-path/example.txt
    
  • config_path(): Returns a config/ path. Examples:
    
    $path = config_path();
    // $path > /var/www/example.org/config
    $path = config_path('my-path');
    // $path > /var/www/example.org/config/my-path/
    $path = config_path('my-path/example.php');
    // $path > /var/www/example.org/my_app/config/my-path/example.php
    
  • storage_path(): Returns a storage/ path. Examples:
    
    $path = storage_path();
    // $path > /var/www/example.org/storage
    $path = storage_path('my-path');
    // $path > /var/www/example.org/storage/my-path/
    $path = storage_path('my-path/example.pdf');
    // $path > /var/www/example.org/my_app/storage/my-path/example.pdf
    
  • config(): Returns a configuration value from a config file under config/. Examples:
    
    // File: config/myconf.php
    return [
        'config_param_1' => Env::get('MY_CONFIG_PARAM_1', 'default value'),
        'config_param_2' => Env::bool('MY_CONFIG_PARAM_2'),
    ];
    
    // Retrieving config values
    $config_one = config('myconf.config_param_1');
    $config_two = config('myconf.config_param_2');
    
To the Top