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 Servicespublic/
is the only accessible to end userssrc/
contains all your source codetemplates/
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:
- Create the route with a basic controller (
/articles/{id}
) - Add a directory with some articles
- 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:
- Serve it directly with Apache, Nginx, ...
- 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:
- The composer package type MUST be
archict-brick
. Unless Core will not be able to load it - 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:
- Pass a string, Archict will use it as response body
- 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 providerroles
➡ 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 returnredirect_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.