Requirements
- PHP >= 8.5.0
- Composer
- MySQL/MariaDB
- Docker (optional)
Installation
-
Create new Beacon project:
composer create-project shworx/beacon my-project -
Enter the project directory:
cd my-project -
Install dependencies:
composer install -
Copy the environment configuration:
cp .env.example .env - Edit environment configuration as you need it
-
Generate an application secret:
php console app:secret -
Copy the generated application secret and add it as value to the
.envfile variableAPP_SECRET= -
Run database migrations:
php console migrate
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
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]
);
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.
- 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,
);
}
}
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):
Example (with query arguments):<a href="{{ route('home') }}">Home</a> Result: <a href="/">Home</a><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():Returnstrueif current user is not authenticated, otherwisefalse. -
currentRoute(): Returns the current route, example:/my-path -
isActiveRoute(): Returnstrueif the passed route name is the current active route, otherwisefalse. 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 functionold() -
errors: Same functionality as the Twig helper functionerrors(). -
_appName: Contains the value of the.envvariableAPP_NAME. -
_appSlogan: Contains the value of the.envvariableAPP_SLOGAN. -
_appVersion: Contains the value of the.envvariableAPP_VERSION. -
_appCopyright: Contains the value of the.envvariableAPP_COPYRIGHT.
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 aapp/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 aconfig/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 astorage/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 underconfig/. 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');