Gustav - PHP Framework
Gustav is a PHP framework for building web applications. It is designed to be simple, object-oriented and using the latest features of PHP.
- Simple - Designed to be easy to use and easy to learn.
- OOP - Designed to be object-oriented.
- Fast - Designed to be fast and lightweight.
- Modern - Designed to use the latest features of PHP.
Features
- HTTP Server
- Typesafety
- Fast Routing
- Middlewares
- Dependency Injection
- Auto-reload during development
- more
Philosophy
Hi, I'm Torsten and like to code. I'm not here to sell you on the next big thing, but rather to share my opinion about current PHP frameworks and why I wanna do things a little differently.
The Overwhelming Maze
I guess many developers have been in these shoes, navigating the maze of PHP frameworks. Complex libraries, random features, inscrutable terminology and the feeling that the kitchen sink got thrown in for good measure. It can be a confusing landscape.
The big players like Laravel and Symfony are awesome, no doubt.
But lack developer experience, I think Laravel especially does a really bad job at it. In my opinion, I should be able to navigate and discover the namespaces and classes of my framework by simply following common sense and known naming patterns.
After 1 minute with Laravel you will encounter a wild Illuminate\Support\Facades\Route
class and this is only the beginning. How do all those terms lead me to creating a Route
for my "Hello World!" endpoint. It's ecosystem is filled with terms like Illuminate
, Eloquent
and Artisan
that just add an unncessecary layer of complexity.
Of cource, once you learned the characteristic of Laravel, Symfony and co you can build whatever you want at enterprise level.
But what if your project isn't an enterprise monolith? What if you want simplicity?
Simplicity
I want to keep it lean. I rather give you a blank canvas and let you paint your project the way you want instead of a printer that comes with a manual. No feature bloat, no complexity - just the simple tools you need.
Have you ever opened a framework's project scaffold only to find it sprouting 15 folders and more files than you can count? It's a recipe for overwhelm. GustavPHP starts you off with a clean slate, not a tangled mess of directories.
That also means, that this framework will not be the swiss knife for all your needs. I don't see this framework ever being shipped with an ORM. Because, honestly, there are some fantastic options available and therefore rather support you in implementing whatever you feel the most comfortable with.
No Magic
We all enjoy a little magic, but not when it leaves us in the dark. Some frameworks operate with a touch of 'magic' that can make your codebase feel like a mystery. Probably why Ruby on Rails never hooked me. I value transparency and control. Your code should be a friend, not a stranger.
Type safety
One aspect that's been missing from many PHP frameworks is robust type safety. The recent PHP versions, especially PHP 7.0 and beyond, have made significant improvements in enhancing type hinting and type declarations.
However, not all frameworks fully embrace these advancements.
Having to define some random-string
on a setter method just to pass random-string
again in a getter method somewhere else with a mixed
return type tires me.
I want to design GustavPHP with type safety in mind, offering a codebase that benefits from strong type hinting, enabling you to catch errors at compile-time rather than at runtime. This ensures a more reliable and maintainable codebase.
Modern PHP
Remember when you were stuck with whatever PHP version your hosting provider decided to throw your way? Those days are luckily over. With containerization tools like Docker, you get to call the shots.
In recent releases the language has undergone a remarkable transformation, introducing features like Just-in-Time Compilation, Fibers, Match Expressions, Attributes, and a plethora of performance enhancements, making it a modern and powerful choice for web developers.
But it feels like the ecosystem is not ready yet - remembering that there are some popular libraries being stuck supporting legacy version like PHP 5.6 and just cannot take advantage of the newest additions.
Final
GustavPHP isn't just a framework; it's my personal take on PHP as a developer, and I'm excited to share it with you. I wanna keep things simple, giving you control, and embracing the modern PHP world. It's your project, and I'm here to help you make it shine.
Getting started
Before creating your first GustavPHP project, you should ensure that your local machine has PHP and Composer installed.
After you have installed PHP and Composer, you may create a new GustavPHP project via the create-project
command:
composer create-project gustav-php/starter example-app
After the project has been created, start GustavPHP's local development server using the serve command:
cd example-app
php gustav dev
Once you have started the Artisan development server, your application will be accessible in your web browser at http://localhost:4201
.
Project Structure
Gustav PHP is flexible in terms of project structure, here you can find informations for the recommended structure that is used with the starter project.
Directory Structure
This is the minimal directory structure to in the starter project.
├─ app/
│ └─ index.php
├─ cache/
├─ public/
├─ src/
│ ├─ Events/
│ ├─ Middlewares/
│ ├─ Routes/
│ ├─ Serializers/
│ └─ Services/
├─ views/
└─ gustav
The app/index.php
is the entrypoint to your application.
The cache/
directory is used for cache files with the views.
The public/
directory contains all static files and are publically accessible in the root.
The src/
directory contains all the components that build your application:
Events/
contains all Event listenersMiddlewares/
contains all MiddlewaresRoutes/
contains all RoutesSerializers/
contains all SerializersSerives/
contains all Services
Configuration
Learn how to configure Docus.
$configuration = new Configuration(
mode: \GustavPHP\Gustav\Mode::Production,
namespace: __NAMESPACE__,
cache: __DIR__ . '/../cache/',
files: __DIR__ . '/../public/',
eventNamespaces: [],
routeNamespaces: [],
serviceNamespaces: [],
serializerNamespaces: []
);
Key | Description |
---|---|
mode | Sets the application in development or production. |
namespace | Sets the application namespace for class discovery. |
cache | Absoulute path to the directory used for cache. |
files | Absoulute path to the directory used for static assets. |
eventNamespaces | Namespace for all additional Event classes. |
routeNamespaces | Namespace for all additional Route classes. |
serviceNamespaces | Namespace for all additional Service classes. |
serializerNamespaces | Namespace for all additional Serializer classes. |
Production
You can enable production mode by setting the environment variable MODE
to production
or change it in app/index.php
.
To start the production serve just run:
php gustav start
Controllers
Controllers are the heart of the framework.
They are responsible for handling incoming requests and returning a response. All controllers must extend the Controller\Base
class.
use GustavPHP\Gustav\Controller;
class DogsController extends Controller\Base
{
}
Routes are defined by attaching the GustavPHP\Gustav\Attribute\Route
Attributes to public controller methods.
use GustavPHP\Gustav\Attribute\Route;
class DogsController extends Controller\Base
{
private $dogs = [
[
'name': 'Rex',
'breed': 'German Shepherd'
]
];
#[Route('/dogs')]
public function list()
{
return $this->json($this->dogs);
}
}
You can also define the HTTP method that the route should respond to adding the Method
enum to the Route
parameters. You can also define the route parameters by adding the Param
attribute to the function parameters.
//...
use GustavPHP\Gustav\Attribute\Body;
use GustavPHP\Gustav\Router\Method;
class DogsController extends Controller\Base
{
//...
#[Route('/dogs', Method::POST)]
public function create(#[Body('name')] string $name)
{
return $this->json([
'name': 'Rex',
'breed': 'German Shepherd'
])
}
}
Now we just need to inizialize the framework and we are ready to go.
use GustavPHP\Gustav\Application;
//...
$app = new Application(routes: [CatsController::class]);
$app->start();
Instead of adding every controller manually, Gustav PHP can also automatically discover all Controllers in the Namespace by using the Configuration. This is done by adding the routeNamespaces
argument to the Configuration
constructor.
$configuration = new Configuration(
routeNamespaces: [
'GustavPHP\Example\Routes'
]
);
$app = new Application(configuration: $configuration);
Full example:
use GustavPHP\Gustav\Application;
use GustavPHP\Gustav\Attribute\Param;
use GustavPHP\Gustav\Attribute\Route;
use GustavPHP\Gustav\Controller;
use GustavPHP\Gustav\Router\Method;
class DogsController extends Controller\Base
{
protected array $dogs = [
'Fido',
'Rex',
'Spot',
];
#[Route('/dogs')]
public function list()
{
return $this->dogs;
}
#[Route('/dogs', Method::POST)]
public function create(#[Param('name')] string $name)
{
$this->dogs[] = $name;
return $this->dogs;
}
}
$app = new Application(routes: [CatsController::class]);
$app->start();
Routing
You use the GustavPHP\Gustav\Attribute\Route
attribute to define an endpoint.
#[Route('/dogs')]
public function list()
By default the GET
method is used, however you can pass a method using the GustavPHP\Gustav\Router\Method
enum.
#[Route('/dogs', Method::POST)]
public function create()
You can also use parameters in your path by surround a path segment with curly braces. You can access a param by using the GustavPHP\Gustav\Attribute\Param
attribute in the arguments.
The argument passed to the attribute must match the one from the desired path segment.
#[Route('/dogs/{dog}')]
public function create(#[Param('dog')] string $id)
Of course you can use multiple parameters.
#[Route('/dogs/{dog}/collars/{collar}')]
public function getCollar(
#[Param('dog')] string $id,
#[Param('collar')] string $collar
)
The framework uses an internal router to match incoming requests to the defined routes.
The router is parsing all routes upfront and stores them in a hashmap on start. This way the router can match the incoming request to the correct route in O(1) time.
Request
Handlers often need access to the client request details. GustavPHP provides access to the request object of the underlying server. We can access the request object by instructing the framework to inject it by adding the GustavPHP\Gustav\Attribute\Request
attribute to the method's signature.
use GustavPHP\Gustav\Attribute\Request;
use Psr\Http\Message\ServerRequestInterface;
#[Route('/dogs')]
public function list(#[Request] ServerRequestInterface $request)
Query Parameters
Query parameters are a common way to pass data to a server through a URL. In the context of the GustavPHP framework, query parameters can be accessed through the request object's getQueryParams()
method or the GustavPHP\Gustav\Attribute\Query
attribute.
Using the request object:
#[Route('/dog')]
public function get(#[Request] ServerRequestInterface $request) {
$query = $request->getQueryParams();
$id = $query['id'];
}
Using the attribute:
#[Route('/dog')]
public function get(#[Query] array $query) {
$id = $query['id'];
}
You can also pass the key in order to get the desired query parameter:
#[Route('/dog')]
public function get(#[Query('id')] string $id) {
// Use the value of the `?id=` query parameter
}
Body
The request body is a part of an HTTP request that contains data that is sent from the client to the server. In the context of the GustavPHP framework, the request body can be accessed through the request object's getBody()
method or the GustavPHP\Gustav\Attribute\Body
attribute.
Using the request object:
#[Route('/dog', Method::POST)]
public function create(#[Request] ServerRequestInterface $request) {
$body = $request->getBody();
$name = $body['name'];
}
Using the attribute:
#[Route('/dog', Method::POST)]
public function create(#[Body] array $body) {
$id = $query['id'];
}
You can also pass the key in order to get the desired query parameter:
#[Route('/dog', Method::POST)]
public function create(#[Body('name')] string $name) {
// Use the value of the `name` query parameter
}
Header
HTTP headers are additional pieces of information that are sent along with an HTTP request or response, providing metadata about the request or response. In the context of the GustavPHP framework, the request body can be accessed through the request object's getHeader($name)
method or the GustavPHP\Gustav\Attribute\Header
attribute.
Using the request object:
#[Route('/dog')]
public function get(#[Request] ServerRequestInterface $request) {
$userAgent = $request->getHeader('User-Agent');
}
Using the attribute:
#[Route('/dog')]
public function get(#[Header] array $headers) {
$userAgent = $headers['User-Agent'];
}
You can also pass the key in order to get the desired query parameter:
#[Route('/dog')]
public function get(#[Header('User-Agent')] string $userAgent) {
// Use the value of the `name` query parameter
}
Data Transfer Object
In order to avoid 10+ arguments for more complex payloads, you can also set the data type from arguments used with the Query
and Body
attributes to a class.
Every public property will be used as a param. Properties with a default value are flagged as optional.
class DogDto
{
public string $name;
public string $breed;
public bool $cute = true;
}
This class can now be used as a DTO like this:
#[Route('/dog')]
public function get(#[Query] DogDto $dogDto) {
}
#[Route('/dog', Method::POST)]
public function create(#[Body] DogDto $dogDto) {
}
Validation
Request validation is achieved by the arguments itself. If a argument for Body
and Query
has no default value, its required.
#[Route('/some')]
public function get(
#[Query('required')] string $required,
#[Query('optional')] string $optional = 'default',
) {
}
The same goes for DTO.
class SomeDto
{
public string $required;
public string $optional = 'default';
}
Additionaly you can use the validate(...)
method of your controller for more granular validation.
In this example $number
must be a value between 0 and 100:
#[Route('/custom-validation')]
public function get(
#[Query('number')] int $number
) {
$this->validate([
[$number, new GustavPHP\Gustav\Validation\General\Integer(min: 0, max: 10)]
]);
}
Response
All routes should return a response to be sent back to the user. GustavPHP provides several different ways to return responses.
HTML
HTML response is used to return HTML content to the user's browser. This is useful when you want to render a web page with dynamic content. To return an HTML response, you can use the html(...)
method of your controller.
#[Route('/html')]
public function index()
{
return $this->html("<h1>Hello World!</h1>");
}
JSON
You can return JSON using the json(...)
method of your controller.
#[Route('/json')]
public function index()
{
return $this->json([
'name' => 'Torsten',
'age' => 30,
'company' => 'Appwrite'
]);
}
XML
You can return XML using the xml(...)
method of your controller.
#[Route('/xml')]
public function index()
{
return $this->xml("<note>Hello World!</note>");
}
Redirect
You can perform a HTTP redirect using the redirect(...)
method of your controller.
#[Route('/redirect')]
public function index()
{
return $this->redirect("/json");
}
Serialize
You can serialize your response payload using the serialize(...)
method of your controller.
#[Route('/serialize')]
public function index()
{
return $this->serialize(new Cat());
}
You can find out more about Serialization here.
View
tbd
Services
Services are classes that can be injected into controllers and services itself. They are defined by extending the Service\Base
class.
use GustavPHP\Gustav\Service;
class Police extends Service\Base
{
public string $icon = '👮♀️';
}
To inject a service into a controller, you need to add the desired service to the controllers constructor.
use DI\Attribute\Inject;
class DogsController extends Controller\Base
{
public function __construcst(protected Police $police)
{
}
#[Route('/police')]
public function police()
{
return $this->police->icon;
}
}
Middlewares
Middlewares are classes that are run before the controller is executed. They are defined by extending the Middleware\Base
class.
class MyMiddleware extends Middleware\Base
{
public function handle(Psr\Http\Message\ServerRequestInterface $request): ServerRequestInterface
{
// do stuff with `$request`
return $request;
}
}
To use a middleware with a controller, you need to add the GustavPHP\Gustav\Attribute\Middleware
attribute to the controllers class.
#[GustavPHP\Gustav\Attribute\Middleware(new MyMiddleware())]
class CatsController extends Controller\Base
//...
You can add information to the request by adding attributes to the request.
public function handle(Psr\Http\Message\ServerRequestInterface $request): ServerRequestInterface
{
$request = $request->withAttribute('from-middleware', 'Hello World!');
return $request;
}
And then get them from the Request in Controllers.
class DogsController extends Controller\Base
{
#[Route('/from-middleware')]
public function police(#[Request] Psr\Http\Message\ServerRequestInterface $request)
{
$info = $request->getAttribute('from-middleware');
return $this->plaintext($info);
}
}
Events
All events must extend the Events\Base
class.
use GustavPHP\Gustav\Events;
class TestEvent extends Events\Base
{
}
Events can be defined by attaching the Event Attributes to a class.
use GustavPHP\Gustav\Attribute\Event;
use GustavPHP\Gustav\Event;
#[Event('test')]
class TestEvent extends Event\Base
{
public function handle(Event\Payload $payload): void
{
$this->log('Event: ' . $payload->getEvent());
}
}
Events can be dispatched from anywhere using.
GustavPHP\Gustav\Event\Manager::dispatch('test', [
'key' => 'value'
]);
Events are automatically added like Routes in the App\Events
namespace.
Views
Views are powered by the Latte template engine and can be used in controller endpoints like this:
#[Route('/')]
public function index()
{
return $this->view(__DIR__ . '/../views/index.latte', [
'title' => 'lorem ipsum',
'content' => 'Lorem ipsum dolor...'
]);
}
And the index.latte
can be used like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{$title}</title>
</head>
<body>
{$content}
</body>
</html>