Introduction

Getting started

Let's create together a small website in this tutorial.

Documentation

Wants to go further? Read detailed documentation of each available features.

Existing Bricks

Documentation of each official Bricks.

New project

In this short tutorial, we'll see how to create a blog with a main page and some articles.

There is some prerequisites before beginning. You need to have PHP 8.2 or newer and composer installed on your computer

First step, create the project itself:

composer create-project archict/archict my_blog
cd my_blog
composer install

This first command create a new directory named my_blog containing all files which comes with Archict:

  • config/ is a collection of YAML file for configuration of Services
  • public/ is the only accessible to end users
  • src/ contains all your source code
  • templates/ contains Twig templates

There is some others files and directories but they are not important now, we'll see what we can do with them later.

Want to see what it looks like now? Type php -S localhost:8080 -t ./public in your terminal and go to http://localhost:8080 in your prefered browser. By default Arhict project comes with 2 pages: the main page you currently see and a second one (just for the example) located at http://localhost:8080/status.

Now we are ready to begin the road to our glorious blog!

Main page

The main concept of Archict is the Brick. The project itself is a Brick and comes with a first class named Application. This class is loaded by Archict and has only one role: create the 2 routes we seen previously (see more in Routing). The one which interest us now is the first one create with this line:

$collector->addRoute(Method::GET, '', new HomeController($this->twig));

It tells to Archict that we want to listen empty path on GET method and use an instance of HomeController as the handler. So now let's see this famous handler.

First thing to notice is that the class implements interface RequestHandler. This interface is provided by the Brick Archict/Router and is mandatory to create a handler. It just needs one method: public function handle(ServerRequestInterface $request): ResponseInterface|string. In our case we simply returns a string generated by Twig rendering.

Let's just simplify it with what we need:

public function handle(ServerRequestInterface $request): string
{
    return $this->twig->render('home.html.twig');
}

And now let go see this template file and also fill it with what we need:

<!DOCTYPE html>
<html>
<head>
    <title>My super blog!</title>
</head>
<body>
    <h1>My super blog!</h1>
    <p>Welcome to the best blog of the galaxy</p>
</body>
</html>

note

I'm very bad on front-end development and design, if anyone can help me provide a better html and css example for this tutorial, please open a PR 🙏

If you go back to your browser and refresh, you can now see your brand new homepage. Feel free to tune it to your convenience.

Articles

Now let's create the page for our articles. Here is the steps we'll follow:

  1. Create the route with a basic controller (/articles/{id})
  2. Add a directory with some articles
  3. Render them with Twig

Let's go!

Create the route and the controller

First, let's create a basic empty controller

# src/Controller/ArticlesController.php
<?php
declare(strict_types=1);

namespace Archict\Archict\Controller;

use Archict\Router\RequestHandler;
use Psr\Http\Message\ServerRequestInterface;

final class ArticlesController implements RequestHandler
{
    public function handle(ServerRequestInterface $request): string
    {
        return 'Article ' . $request->getAttribute('id');
    }
}

And then declare our route in Application with

$collector->addRoute(Method::GET, '/article/{id:\d+}', new ArticlesController());

/article/{id:\d+} matches all uri beginning with /article/ followed by a number, we call this number id so we can get it back in the controller via the attributes of the request.

If you go to http://localhost:8080/article/42, you should see a white page with just Article 42 writed.

Add some articles

In a real blog, we should implement a database and store our existing articles inside, but it's not the matter of this tutorial so we'll go to a shorter path: filesystem. We will store our articles inside public/articles and name them with their ids. For example, article 42 will have path public/articles/42.html.twig. I let you create some Twig templates corresponding to our system and then we can tune the controller to get them.

Twig rendering

The controller need to:

  • get the seeked article id
  • check if it exists
  • render it
# src/Controller/ArticlesController.php
<?php
declare(strict_types=1);

namespace Archict\Archict\Controller;

use Archict\Archict\Services\Twig;
use Archict\Router\HTTPExceptionFactory;
use Archict\Router\RequestHandler;
use Psr\Http\Message\ServerRequestInterface;

final readonly class ArticlesController implements RequestHandler
{
    public function __construct(private Twig $twig)
    {
    }

    public function handle(ServerRequestInterface $request): string
    {
        // Get article id
        $article_id = $request->getAttribute('id');

        // Check it exists
        $filename = __DIR__ . '/../../public/articles/' . $article_id . '.html.twig';
        if (! file_exists($filename)) {
            throw HTTPExceptionFactory::NotFound("Article $article_id not found");
        }

        // Render it
        return $this->twig->render($filename);
    }
}

Don't forget to add the argument to the controller constructor in Application

$collector->addRoute(Method::GET, '/article/{id:\d+}', new ArticlesController($this->twig));

Now if you go to http://localhost:8080/article/42, you should see you article. You can replace 42 by any number, if the article doesn't exists you'll get the message that the page doesn't exist.

Deployment

Until now, we only have seen our blog locally, but it's blog, the whole planet need to see it. Let's deploy it!

You have 2 options:

  1. Serve it directly with Apache, Nginx, ...
  2. Use Docker

Apache/Nginx

If you use Apache, you just have to redirect all requests to public/index.php.

For Nginx, you'll need to install fastcgi. You can take inspiration from the existing nginx.conf file at the root of project.

Docker

Archict comes with a Dockerfile and a docker-compose.yml to help you. The Dockerfile uses the php-fpm image, install nginx and copy Archict files. The docker-compose.yml uses this image and map port 80 to 8080. So basically you just need to run docker compose up -d. Feel free to update both files and even the nginx.conf file along your needs.

Brick

In Archict a Brick is a composer package consisting of a collection of Service and Event. Through Archict/Core (which is also a Brick) they can be loaded and interact between them.

In the rest of this documentation all core features are described. Some features from official Bricks are also detailled.

Create a Brick

Creating a Brick is pretty simple. A template is provided on Github https://github.com/Archict/brick-template, but it is also possible to begin from scratch.

As said previously, a Brick is a composer package. But there is 2 points to respect:

  1. The composer package type MUST be archict-brick. Unless Core will not be able to load it
  2. It SHOULD require package archict/brick. It's not mandatory but interface to create Service will not be available.

And that's all!

Services

A Service is a PHP class with attribute Archict\Brick\Service:

<?php

use Archict\Brick\Service;

#[Service]
class MyService
{
}

When Core will load Bricks, this class will be instantiate. If it depends on other Service, by adding them to the constructor Archict will inject them.

# AnotherService.php
#[Service]
class AnotherService
{
}

# MyService.php
#[Service]
class MyService
{
    public function __construct(AnotherService $another_service)
    {
    }
}

Root package will always be considered as a Brick (independently of its package type) and loaded by Core.

Configuration

Services can be configured via a yaml config file. This file MUST be located in root config dir. By default it will be named with the lowered class name (myservice.yml for example).

Configuration of a Service is defined with a data class, for example

class MyConfiguration
{
    public string $foo;
}

And then specified in Service attribute

#[Service(MyConfiguration::class, 'my_config.yml')]
class MyService
{
    public function __construct(MyConfiguration $configuration)
    {
    }
}

The configuration can be injected in the Service constructor. Default config file name can also be overrided with the second argument of Service attribute.

When defining a configuration for a Service, a default configuration MUST be provided. In the previous example case, the file config/my_config.yml MUST exists and contains a default configuration. For example:

foo: "bar"

The file can be empty if a default value is provided in the configuration class

class MyConfiguration
{
    public string $foo = 'bar';
}

Archict uses symfony/yaml and cuyz/valinor to parse then map yaml file. It a problem occur during this process, the thrown exception is not catch to let the developer fix it.

When a Brick in dependencies is loaded, it will first try to get configuration file in config directory of the root package and then get the default one in Brick package.

Events

Listening

A Service can listen to events and it's very simple:

use Archict\Brick\ListeningEvent;
use Archict\Brick\Service;

#[Service]
class MyService
{
    #[ListeningEvent]
    public function listenToSomeEvent(MyEvent $event): void
    {
    }
}

It just need a method with the attribute ListeningEvent. Archict will infer which event is listened by getting type of first parameter. Note that the method MUST have one and only one parameter, the event listened which is an object.

Dispatching

Dispatching of Events is done by using Archict\Core\Event\EventDispatcher::dispatch(object). EventDispatcher is a Service provided by Archict/Core, it can be retrieved from the dependency injection of another Service. The dispatcher will returns the object passed to the method modified by all the listeners. The calling order of listeners cannot be predicted, be vigilant on that.

Official Bricks

Here is a list of Bricks developed by Archict:

Archict/Core

In the Land of PHP where the Shadows lie.

One Brick to rule them all, One Brick to find them,

One Brick to bring them all, and in the darkness bind them

In the Land of PHP where the Shadows lie.

Heart of Archict, this library load and manage Bricks.

Usage

<?php

use Archict\Core\Core;
use Archict\Core\Event\EventDispatcher;

$core = Core::build();
// Load Services and Events
$core->load();

// Access to all Services
$manager    = $core->service_manager;
$dispatcher = $manager->get(EventDispatcher::class);
$dispatcher->dispatch(new MyEvent());

That's all!

Please note that if your package is also a Brick, it will scan only src and include directory for Services.

Supplied Services

ServiceManager

With ServiceManager::get(class-string) you can retrieve any Service. But it's better if you use automatic Service injection in your own Service constructor.

EventDispatcher

With EventDispatcher::dispatch(mixed) you can dispatch an Event to all its listeners.

CacheInterface

Implementation of PSR-16 cache system.

Brick

To build something, you need bricks

Base library of Archict framework

What is a Brick?

A Brick is the base component of Archict. It consists on a collection of Services which can have a Configuration and listen to Event.

How to create a Brick?

In technical terms, a Brick is a composer package with the archict-brick type. The easiest way to create your own Brick is to use our template.

If you bring a look at the composer.json content, you can see this line:

{
  "type": "archict-brick"
}

This line is super mega important, it's the one telling to Archict that your package is a Brick. Without it, your Services will never be loaded.

You also need to depend on this package to have all the necessary classes to create your Brick.

Services

Creating a Service is pretty straight forward, just put the attribute Service on your class:

<?php

use Archict\Brick\Service;

#[Service]
final class MyService {}

One of the feature you can have with Services, is dependencies injection. Your Service can depend on some other Services (from other Bricks for example). For that, just add them in your constructor and Archict will inject them just for you.

<?php

use Archict\Brick\Service;

#[Service]
final readonly class MyService 
{
    public function __construct(
        private AnotherService $another_service,
    ) {}
}

Services configuration

Sometimes, you need your Service to be configurable by another developer. For that you can give them configuration file. For that you need to do several things.

First you need to create a data class which store the configuration variables:

<?php

final readonly class MyConfiguration 
{
    public function __construct(
        public int $nb_workers,
    ) {}
}

Please note that only public members will be considered as part of the configuration.

Then specify to your Service it can use this class as configuration (instance of your configuration can also be injected in the Service constructor):

<?php

use Archict\Brick\Service;

#[Service(MyConfiguration::class)]
final readonly class MyService 
{
    public function __construct(
        private MyConfiguration $config,
    ) {}
}

Finally, provide a default config file (in YAML format) in the config folder at the root of your package. This file will be used as default config unless another config file is provided to Archict. By default, the file is named with your Service class name lowercased (myservice.yml), you can change this behavior by specifying the filename:

<?php

use Archict\Brick\Service;

#[Service(MyConfiguration::class, 'foo.yml')]
final readonly class MyService 
{
}

Events

To bring some life to your Services, they can listen to Events and even dispatch some.

Listening to an Event is pretty easy, just add the ListeningEvent attribute to a public method of your Service. Archict will know which Event you are listening by getting the argument type of the method (so you can name your method as you want):

<?php

use Archict\Brick\ListeningEvent;
use Archict\Brick\Service;

#[Service]
final class MyService
{
    #[ListeningEvent]
    public function fooBarBaz(MyEvent $event): void
    {
        // Do something with $event
    }
}

Dispatching an Event need some steps. First you need to have an Event class:

<?php

final class MyEvent {}

Then add the package archict/core to your dependencies. It contains the Service EventDispatcher which is necessary to dispatch an Event. You can now use it in your Service to dispatch your Event.

<?php

use Archict\Brick\Service;
use Archict\Core\Event\EventDispatcher;

#[Service]
final readonly class MyService
{
    public function __construct(
        private EventDispatcher $dispatcher,
    ) {}
    
    public function someMethod(): void
    {
        $this->dispatcher->dispatch(new MyEvent());
    }
}

How to use Bricks?

For that, let's go see Archict/core.

Archict/Router

Usage

This Brick allows you to setup route with a request handler and add some middleware. Let's see how to do that!

A simple route ...

There is 2 way for creating a route: Having a whole controller class, or a simple closure.

First you need to listen to the Event RouteCollectorEvent:

<?php

use Archict\Brick\Service;
use Archict\Brick\ListeningEvent;
use Archict\Router\RouteCollectorEvent;

#[Service]
class MyService {
    #[ListeningEvent]
    public function routeCollector(RouteCollectorEvent $event) 
    {
    }
}

Then just create the route with the Event object:

With a closure:

$event->addRoute(Method::GET, '/hello', static fn() => 'Hello World!');

The closure can take a Request as argument, and return a string or a Response.

With a controller class

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

$event->addRoute(Method::GET, '/hello', new MyController());

class MyController implements RequestHandler {
    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        return ResponseFactory::build()
            ->withStatus(200)
            ->withBody('Hello World!')
            ->get();
    }
}

Your controller must implement interface RequestHandler. The method handle can return either a string or a Response.

Please note that you can define only one handler per route.

The first argument of method RouteCollectorEvent::addRoute must be a string of an allowed HTTP method. Enum Method contains list of allowed methods. If your route can match multiple method you can pass an array of method, or Method::ALL.

The second argument is your route, it can be a simple static route, or a dynamic one. In this last case, each dynamic part must be written between {}. Inside there is 2 part separated by a :, name of the part, and pattern. The name of the dynamic part allow you to easily retrieve it in Request object.

The pattern can be empty, then it will match all characters until next / (or the end), or it can be a regex with some shortcuts:

  • \d+ match digits [0-9]+
  • \l+ match letters [a-zA-Z]+
  • \a+ match digits and letters [a-zA-Z0-9]+
  • \s+ match digits, letters and underscore [a-zA-Z0-9_]+

You can also have an optional suffix to your route with [/suffix].

Here is an example: /article/{id:\d+}[/{title}].

If something went wrong along your process, you can throw an exception built with HTTPExceptionFactory. The exception will be caught by the router and used to build a response.

... with a middleware

Sometimes you have some treatment to do before handling your request. For that there is middlewares. You can define as many middlewares as you want. To define one, the procedure is pretty the same as for a simple route:

<?php

use Archict\Brick\Service;
use Archict\Brick\ListeningEvent;
use Archict\Router\RouteCollectorEvent;
use Psr\Http\Message\ServerRequestInterface;

#[Service]
class MyService {
    #[ListeningEvent]
    public function routeCollector(RouteCollectorEvent $event) 
    {
        $event->addMiddleware(Method::GET, '/hello', static function(ServerRequestInterface $request): ServerRequestInterface {
            // Do something
            return $request
        });
        // Or
        $event->addMiddleware(Method::GET, '/hello', new MyMiddleware());
    }
}

If you define your middleware with a closure, then it must return a Request. If it's an object, then your class must implement interface Middleware:

use Psr\Http\Message\ServerRequestInterface;

class MyMiddleware implements Middleware
{
    public function process(ServerRequestInterface $request): ServerRequestInterface
    {
        return $request;
    }
}

You can do whatever you want in your middleware. If something went wrong, the procedure is the same as for RequestHandler.

Special response handling

By special, we mean 404, 500, ... In short HTTP code different from 2XX. By default, Archict will use ResponseHandler which just set the corresponding headers. Via the config file of this Brick you change this behavior:

error_handling:
  404: \MyHandler
  501: 'Oops! Something went wrong'

You have 2 choices:

  1. Pass a string, Archict will use it as response body
  2. Pass a class string of a class implementing interface ResponseHandler, Archict will call it. For example:
<?php

use Archict\Router\ResponseHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class MyHandler implements ResponseHandler
{
    public function handleResponse(ResponseInterface $response, ServerRequestInterface $request): ResponseInterface
    {
        $factory = new \GuzzleHttp\Psr7\HttpFactory();
        return $response->withBody($factory->createStream("Page '{$request->getUri()->getPath()}' not found!"));
    }
}

Your class can also have dependencies, as for a Service just add them to your constructor. These dependencies must be available Service.

Firewall

Control access to your resources

How to use

The majority of the work is inside the config file firewall.yml:

providers:
  my_provider: \Class\Name\Of\My\User\Provider
access_control:
  - path: ^/admin # Path to match (a regex)
    provider: my_provider
    roles: [ "ADMIN" ] # Roles user need to have to see resources
    error: 404 # If user not authorized, then return this error code
  - path: ^/profile
    provider: my_provider
    roles: [ "USER" ]
    redirect_to: /login # If user not authorized, then return to this uri

Let's go in details!

User provider

To help firewall to get current user, you need to give it a User provider.

This Brick provides you the interface \Archict\Firewall\UserProvider:

<?php

namespace Archict\Firewall;

interface UserProvider 
{
    public function getCurrentUser(ServerRequestInterface $request): UserWithRoles;
}

The class you pass in the config must implement this interface. It can have dependencies like a Service, they will be injected during instantiation.

User is an interface also provided by this Brick:

<?php

namespace Archict\Firewall;

interface UserWithRoles
{
    /**
     * @return string[]
     */
    public function getRoles(): array;
}

Access control

This config tag must contain an array of rules.

Each rule must have at least the path tag. This tag define the path to match, it can be a pattern with the same rules as in Archict\router.

Then you have the choice between let the firewall check if user can access the resource (the check is based on user roles), or implement your own checker.

Firewall checker

If you choose to use firewall checker, then you must provide these 2 tags:

  • provider ➡ One of the previously defined provider
  • roles ➡ An array of string. User must have one of these roles to access resource

Then you can define the behavior with one these rules (only one):

  • error ➡ a HTTP error code to return
  • redirect_to ➡ return a 301 response with the specified uri

Your own checker

To use your own checker, your class must implement this interface:

<?php

namespace Archict\Firewall;

interface UserAccessChecker
{
    public function canUserAccessResource(ServerRequestInterface $request): bool;
}

This method returns true if user is authorized to see resource. It can throw an exception the same way as defined in Archict\router. Implementation of this interface can have some dependencies in its constructor, they will be injected during instantiation.

Then you can provide the class name to your rule with the tag checker:

access_control:
  - path: some/path
    checker: \My\Own\Checker

You can also provide one of the behavior tag (see Firewall checker) in case your method returns false.

Community Bricks

Here is a list of Bricks developed by the community. They are not maintained by Archict team.

note

There is no community Brick for now

You have developed one, feel free to open a PR or an issue to add it to this list

Contributors

Here is a list of the persons who have contributed to Archict.

Feel free to contribute too. If it's already the case and you're missing from the list, you can add yourself by opening a PR or an issue.