Micrus Tiny, yet full-stack, PHP framework

A framework doesn't have to be overly complex! Micrus provides you with a quick, easy and comfortable way of creating neatly structured, modular MVC websites, which can be easily extended and configured.

Our goal is to keep the framework as simple as possible, while offering all the most important features.

Full documentation is available at docs.avris.it/micrus

$ composer create-project avris/micrus-demo my_new_project
$ cd my_new_project
$ bin/env
$ bin/micrus db:schema:create
$ bin/micrus db:fixtures
$ yarn
$ yarn server
$ php -S localhost:8070 -t public/

Your website should be available under:

http://localhost:8070

Disclaimer

I believe that an example is worth a thousand words. That's why Micrus Demo Project is the main source of the documentation for the framework, while the following document might be sketchy and incomplete.

And also: Micrus should be simple enough to just quickly check out parts of its source instead of reading the docs.

Getting started

MVC

Micrus is a full-stack MVC framework. What does that mean?

Framework is a special kind of library, that's supposed to organize your code, enforce the project's workflow and integrate it with other libraries.

One of the most popular ways of organizing the code of web applications is the MVC architectural pattern, which stands for Model-View-Controller. It means that three main "layers" of the project are:

There are microframeworks and full-stack frameworks. The first one is just a loose frame that doesn't include a lot of features and leaves the developer with big possibilities of adjustment. The latter is fully equipped with features and has a concrete structure. Micrus tries to be small and simple, while also being fully stacked and extendable.

HTTP and application flow

When opening a website in the browser, your computer communicates with the server via the HTTP protocole. It's fairly simple, text-based and stateless. The browser sends a Request, which is just a text document containing set of key-value headers, and a content.

Let's say it's a request with POST method to http://myawesomepage.com/post/14/edit with content that is an array of values such as title, text, publication date etc. Micrus wraps this request in an abstraction layer, allowing you to easily access all the received data.

Micrus app has a routing table that binds specific URLs to specific controllers. This table says that the URL POST /post/14/edit should be handled by the action editAction inside the controller PostController. Before executing that controller, Micrus might execute additional code (see: Event Dispatcher). For instance, it checks, if the user has the credentials to edit posts. It retrieves a session key from headers and either authorises the user to edit the post, or redirects to the login page.

The controller receives a Request object and has to return a Response object. It's gonna contain the HTML code with, among others, a message that the post has been successfully updated (or not). It might also set some values in the headers or modify the default 200 response code (like the famous 404 if the post with id=14 doesn't exist in the database – but that case would already be handled by Micrus). So, to sum up: the Controller receives a Request, then does some operations on the Model(s) (maybe using some external services), asks the View to render an expected HTML code, and then returns a Response with that HTML.

Read more about the Request/Response abstraction layer in Avris Http.

Directory structure

- public
   - index.php
- config
   - services.yml
   - prod
      - services.yml
- src
   - Command
   - Controller
   - Form
   - Entity
   - Service
   - App.php
- templates
     - layout.html.twig
- translations
   - app.en.yml
- bin
  - micrus
- assets
- var
  - cache
  - logs
- tests
- vendor
- .env

The public directory should be the only one that's published by the web server. It contains all the frontend data, such as css files, scripts and images, as well as the front controller. Front controller (index.php) is a tiny php files – a "gateway" to all the rest of the backend code, which is hidden in different directories, so in case of server misconfiguration it doesn't leak.

src directory is a place for your PHP code. It corresponds to the App namespace (PSR-4, for instance class App\Entity\Post should be located in src/Entity/Post.php).

The following structure is recommended, but not always required (any service can be a controller or a command, regardless of its location – but on the other hand Micrus Doctrine looks for entities only in the Entity directory):

templates contains the templates (.phtml, .twig or else, depending on which template engine you're using).

config, obviously, contains all the config files for your project. They use YAML format. More about the config in the config section.

The vendor directory contains all the 3-rd party code: Micrus itself and other libraries.

The var directory should contain all the files that get created during runtime: cache, logs, user uploads etc. Make sure that is writable by the server!

The tests directory should contain all the tests for your project, and preserve the namespace structure of the src dir.

Directory bin should contain the console executables. Located there is bin/micrus, which executes Micrus-related commands like clearing cache or creating the database schema. This also includes your own custom commands.

Only public and var directories could be writable – the rest should be treated as read-only once deployed to a server.

Modules

Micrus is built out of modules so that functionalities be easily added, removed and switched. A module contains a set of files like PHP classes, tests, Twig templates, translations, configs etc. The best thing about the modules is that everything is a module, including your whole app and Micrus itself. This means that the directory structure of a module is the same as the one presented above. All the configs, templates, translations etc. provided by modules can be loaded in a consistent manner.

For instance the Templater service receives a list of all the modules and checks all of them for the existence of a templates directory. If it's there, this directory is registered as a source of templates.

You can register and unregister the enabled modules in the src/App.php file:

protected function registerModules(): iterable
{
    // \Avris\Micrus\MicrusModule is added automatically as the first one
    yield new \Avris\Micrus\Twig\TwigModule;
    yield new \Avris\Micrus\Doctrine\DoctrineModule;
    yield new \Avris\Localisator\LocalisatorModule;
    yield new \Avris\Forms\FormsModule;
    yield $this;
}

Modules lower in this least overwrite the ones higher. For instance MicrusModule can define a service, but TwigModule could modify this definition a little bit, and then your App will unregister this service completely. That's why your App module ($this) should be the last one, so that you can overwrite whichever default config, templates etc. are provided by external libraries.

Environmental variables

Sensitive and environment-dependent configs (like database connection, mailer configuration etc.) should be kept in the environment variables. On the production/staging/qa servers they should be provided by the webserver, but on local dev environment they could be loaded form the .env using Avris Dotenv.

When you run bin/env, Micrus will ask all registered modules for the expected environmental variables, will ask you for their values, and store them in the .env file.

Two important env vars are APP_ENV and APP_DEBUG. The first one defines a name of your environment (dev, test, prod etc.) so that you can use different configs for different environments. The other one is boolean (0 or 1) and determines: whether debug messages or nice 404 etc. pages should be shown, whether to cache configs or not etc.

You can pass an env var to a service by using its name between percentages: %APP_ENV%. They can also be autowired, if an argument name in you constructor starts with $env: bool $envAppDebug.

Config

Micrus's config is composed out of the parts provided by its modules. For instance if one one module provides the following files:

config/services.yml:

Module1\Service:
    public: true

Avris\Micrus\View\TemplaterInterface: Avris\Micrus\View\Templater

config/mailer.yml:

sender: foo@bar.com

And the other module provides the following config/services.yml:

Avris\Micrus\View\TemplaterInterface: Module2\Templater
Module2\Templater:
    arguments:
        $env: '%APP_ENV%'

Then the resulting config variable will look like this:

[
    'services' => [
        'Module1\Service' => ['public' => true],
        'Avris\Micrus\View\TemplaterInterface' => 'Module2\Templater',
        'Module2\Templater' => [ 'arguments' => ['$env' => 'prod'] ],
    ],
    'mailer' => [
        'sender' => 'foo@bar.com',
    ],
]

If a module provides a config/prod/services.yml file, then its content will also be merged to the config, but only if APP_ENV = prod.

You can import other config files:

config/services.yml

_import: ['_logger']

This will paste the full content of config/_logger.yml to config/services.yml. There won't be a separate _logger entry in the config, since files starting with _ are ignored.

To delete an entry that was added by another module, you can specify it like this:

_delete: [key_to_be_removed]

You can also specify default values for all the entries in a specific file:

services.php:

_defaults:
    public: true

All the services defined in this file will be public by default.

You can pass part of the config to a service by using the syntax: @config.mailer. It can also be autowired, if an argument in you constructor is a Bag object and its name starts with $config: Bag $configMailer.

Routing and controllers

Routing

Routing is a table of rules that determine which URL should be handled by which controller. It can be specified in config/routing.yml and looks like this:

home:
  pattern: /
  target: App\Controller\HomeController/home

helloUser:
  pattern: /hello/{id}
  target: App\Controller\HomeController/helloUser
  requirements: { id: \d+ }

helloName:
  pattern: /hello/{name}
  target: App\Controller\HomeController/helloName
  defaults: { name: Stranger }
  methods: GET

Or with a shorthand notation:

home:       / -> Home/home
helloUser:  /hello/{int:id} -> Home/helloUser
helloName:  GET /hello/{name=Stranger} -> Home/helloName

There should always be a route called home (it's used for instance as a fallback redirect after logging out or switching locale of the page). For every route, fields pattern and target are required.

In our example, when URL / is requested, router gives control to the controller App\Controller\HomeController/home (that means: service App\Controller\HomeController, method homeAction).

If the requested URL doesn't match /, the next route is checked. This one catches everything that starts with /hello/ and then follows with one or mode decimals (because of the requirements field). URLs /hello/5 and /hello/123 do match, but /hello/foo, /hello/123/456 or /hello don't.

The next route catches everything like /hello/Thomas, /hello/Josh or /hello/Meteo or even /hello alone – it this case parameter name is set to default value Stranger.

Example flows:

If method is set (to GET, POST, PUT or DELETE), router will only accept that given HTTP method for this route.

Routes can also be defined using Annotations.

Handling requests

The simplest controller to handle the request matching our helloName route looks like this:

public function helloNameAction(string $name)
{
    return new Response(sprintf('Hello, %s!', $name);
}

It receives $name from the URL and creates a Response object. Every controller must return an object implementing Avris\Micrus\Controller\Http\ResponseInterface.

There are some shorthands available for generating responses:

The base Controller class also provides some other helpers:

Automatching

Based on what parameters does your action expect, Micrus can automatically pass some values to it. For example:

// route: /test/{id}/{name}
public function testAction(RequestInterface $request, User $user, string $name) { ... } 

This action requires RequestInterface, which is a public service in the container, so it will be passed. The next value has a type User, and if you have Micrus Doctrine installed and User is a Doctrine entity, then it will be looked up in the database by id. The $name parameter will be simply transfered from the URL to the action.

You can add your own automatchers creating a service that implements MatchProvider interface and is tagged matchProvider (will be tagged automatically).

Model

Micrus is ORM agnostic, so you can use whatever ORM you want (Doctrine is supported). For all the ORM-specific stuff, check out their documentation.

All the Entity classes should to be put in the Entity sub-namespace of a given module.

View

Micrus is template engine agnostic (plain PHP and Twig are supported).

Whichever one you're using, those Micrus-specific variables, functions and filters will be available:

Note that Micrus's extensions use Twig, not plain PHP. They can provide their own variables, functions, filters etc. to Twig.

We recommend using manifest-based asset versioning. If your asset compiling tool, like Webpack, generates a manifest.json file, put it in public/assets/manifest.json (or whichever part you specify in @config.assets.manifest). Micrus will use that file to locate the assets referenced with {{ asset('filename.js') }}.

If the manifest is not there, it will just look for them in the public directory.

Forms

Without a framework, when you want to create a form, there are many things you must take into consideration: binding an existing object (if any) to each sparate field of the form, validating them after user has submited the form, if invalid redisplaying it if with POST data bound and with validation errors...

Micrus adds an abstraction layer that handles all of that. You just need to define the list of fields you need and their cofiguration options. You'll get an object that will handle everything for you. Just use it in the controller and display it in the view.

Let's look at an example (using Doctrine and Twig):

<?php
namespace App\Form;

use App\Model\User;
use Avris\Micrus\Model\Assert as Assert;
use Avris\Micrus\Model\Form;
use Avris\Micrus\Tool\Crypt;

class RegisterForm extends Form
{
    public function configure() {
        $this
            ->add('username', 'Text', [
                'placeholder' => l('entity.User.custom.usernameRegex'),
            ], [
                new Assert\NotBlank(),
                new Assert\Regexp('^[A-Za-z0-9_]+$', l('entity.User.custom.usernameRegex')),
                new Assert\MinLength(5),
                new Assert\MaxLength(25),
                new Assert\Unique(
                    $this->options->get('orm'), $this->object, 'user', 'username',
                    l('entity.User.custom.usernameTaken')
                ),
            ])
            ->add('email', 'Email', [], new Assert\NotBlank())
            ->add('doPasswordsMatch', 'ObjectValidator')
            ->add('password', 'Password',
                [new Assert\NotBlank(), new Assert\MinLength(5)]
            )
            ->add('passwordRepeat', 'Password', [], new Assert\NotBlank)
            ->add('agree', 'Checkbox', [
                'label' => '',
                'sublabel' => l('entity.User.custom.agreement')
            ], new Assert\NotBlank)
        ;
    }

    public function doPasswordsMatch($user)
    {
        return $user->password === $user->passwordRepeat
            ? true
            : l('entity.User.custom.passwordMismatch');
    }
}

Handled by the controller like this:

public function registerAction()
{
    if ($this->getUser()) {
        return $this->redirectToRoute('myAccount');
    }

    $form = new RegisterForm(new User(), $this->container);

    $form->bind($this->getData());
    if ($form->isValid()) {
        $user = $form->getObject();
        $user->setPassword($this->getService('crypt')->hash($user->getPassword()));
        $this->getEm()->persist($user);
        $this->getEm()->flush();
        $this->getService('securityManager')->login($user);
        return $this->redirectToRoute('myAccount');
    }

    return $this->render(['form' => $form->setStyle(new Bootstrap2)]);
}

And Displayed in the view like that:

<form method="post" class="form">
    {{ form|raw }}
    <div class="col-lg-offset-2">
        <button type="submit" class="btn btn-primary">Register</button>
    </div>
</form>

The method configure() is executed right after creating a form. In there you can define all the fields.

Widgets

The add($name, $type = 'Text', $options = [], $asserts = [], $visible = true) method lets you add a field to your form. $name parameter must be unique because it will be the name of object's property. $type is a string that defines which widget should be used:

Choice widget has four remarkable options:

There are to special widgets:

Asserts

Available asserts are:

Many widgets automatically add a relevant assert, so you don't have to.

Styles

Use setStyle to change how the form is rendered to HTML. For instance with ->setStyle(new Bootstrap2) you get each widget wrapped into Bootstrap classes with 2 columns for label and 10 for the widget. Available styles are: Bootstrap, Bootstrap1, Bootstrap2, Bootstrap3, BootstrapHalf and BootstrapMini. You can create your own by extending class Avris\Micrus\View\FormStyle\Formstyle taking the existing ones as an example.

Iterating

It's sometimes useful not to display the whole form at once, but with some chunks. For instance:

<form method="post" class="form row">
    <div class="col-lg-4">
        {% for widget in form.iterate(null, 'widget3') %}
            {{ widget|raw }}
        {% endfor %}
    </div>
    <div class="col-lg-4">
        {% for widget in form.iterate('widget4', 'widget6') %}
            {{ widget|raw }}
        {% endfor %}
    </div>
    <div class="col-lg-4">
        {% for widget in form.iterate('widget7') %}
            {{ widget|raw }}
        {% endfor %}
    </div>
</form>

The ->iterate($start, $stop) function comes in handy here. If you omit the $start argument, it will start iterating from the beginning, and if you omit $stop, it will go to the very last widget.

Services

Dependency Injection

Services are classes that are supposed to be shared throughout the application, that do a specific job (like handling event or sending emails) and that generally (but necessarily) have at most one instance in the system.

Micrus uses Avris Container. Please check out its documentation to learn about how it works. All that Micrus itself adds to this Container implementation is the ability to configure it using .yml files like this:

App\:
  dir: '%MODULE_DIR%/src/'
  exclude:
    - '#^Entity/#'

App\Service\Mailer:
  params: [@config.parameters.?mailer]
  calls:
    - [setLogger, [@?Psr\Log\LoggerInterface]]
  tags: [defaultParameters]

App\Service\ResponseListener:
  arguments: 
    $logger: @Psr\Log\LoggerInterface
  events: [response]

App\Service\WaveTwigExtension:
  tags: [twigExtension]

countedProduct:
  class: App\Service\CountedProduct

countedProductFactory:
  class: App\Service\CountedProduct
  factory: true

Console tasks

All the actions that don't require GUI, like administrative stuff (load fixtures, manual password generation) or scheduled jobs (using cron) can be run from the console on a server. Just cd to the project directory and run php bin/micrus. You will see a list of available commands to run.

Tasks in Micrus are just an extension of a great tool – Symfony Console Component. Head to its documentation for more information.

As for the Micrus specific stuff: for your custom tasks to be visible in bin/micrus, they have to reside in App\Task namespace, end with *Task and extend Avris\Micrus\Console\Task. Inside them you will have access to $this->container, $this->env, $this->em and $this->rootDir.

Event Dispatcher

Micrus uses Avris Dispatcher. Please check out its documentation to learn about how it works.

Micrus provides and triggers some events that you can listen to and intercept the execution of your app:

Also for the CLI commands

To create your own event, just extend Avris\Micrus\Tool\Bootstrap\Event.

Console tasks

All the actions that don't require GUI, like administrative stuff (load fixtures, manual password generation) or scheduled jobs (using cron) can be run from the console on a server. Just cd to the project directory and run bin/micrus (or php bin/micrus on Windows). You will see a list of available commands to run.

Micrus uses a great tool for that – Symfony Console Component. Head to its documentation for more information.

To register a command just create a service extending Command and tagged command (will be tagged automatically).

Localization

Locale is a set of data that lets you deliver your website adjusted to a given localization. It includes mainly the translations, but also currency, data format and so on.

Locale's identifier is: the ISO 639-1 language code, then an underscore (_), then the ISO 3166-1 alpha-2 country code. Or is might be the language code alone.

Locales are placed in app/Locale directory in form of YAML files, for instance:

- app
    - Locale
        - en.yml
        - en_UK.yml
        - en_UK.yml
        - pl.yml

en.yml would contain words and phrases shared by both British and American English, while en_UK could look like this:

color: Colour
dateFormat: d/m/Y
currency:
  before: £

and en_US like this:

color: Color
dateFormat: m/d/Y
currency:
  before: $

pl should of course all the data in one file, since it has no "children".

In the app/Config/config.yml file you should to set the list of allowed locales and a fallback:

locales:
  en_GB: English (GB)
  en_US: English (USA)
  pl: Polski
  de: Deutsch
fallbackLocale: en_GB

Now, when a word is to be translated by Micrus' Localizator, and current user's locale is set to en_UK, this is what happens:

  1. Word is looked up in en_UK.yml and returned if found,
  2. Otherwise it's looked up in "parent" locale, en.yml, and returned if found,
  3. Otherwise – in the fallback, pl.yml, and returned if found,
  4. If translation still not found, the original word is returned untranslated.

If user's current locale were pl, then only pl.yml would be checked.

If no locale is set in the session, Micrus will try to guess the best one, based on browser's headers and the list of available locales.

Usage

Localization is a thing that's really widely used. Virtually every part of the application might want to translate some strings. And rightfully injecting the Localizator service everywhere could be incredibly annoying... That's why localization, exceptionally, has it's global function, l(word, replacements = []). It creates a LocalizedString object, which gets translated when casted to simple string.

$this->addFlash('success', l('entity.Post.actions.createSuccess', ['%title%' => $post->getTitle()]));

If your locale contains this entry:

entity:
  Post:
    actions:
      createSuccess: Post "%title%" has been successfully created!

And your post has a title "Lorem ipsum", then the displayed flash message will say:

Post "Lorem ipsum" has been successfully created!

The localizator can also be used directly:

$this->getService('localizator')->get('entity.Post.actions.createSuccess', ['%name%' => $post->getName()]);

And in the view (in case of Twig):

{{ 'entity.Post.actions.createSuccess'|l({'%title%': post.title}) }}

To set a locale:

$request->getSession()->set('_locale', $locale);

where $locale is simply a string identifying locale. Or you can simply generate a route to do it for you:

<a href="{{ route('locale_change', { locale: 'en_UK' }) }}">

Locale sets, conventions

Your app/Locale directory doesn't have to be the only one. External libraries might offer their own. As a matter of fact, Micrus provides one itself (for the validators). Keeping the locales organized in tree structures helps avoiding conflicts between them. Entries in app/Locale will always overwrite any other locale set.

In order for the translations to be reused amid modules, please stick to the convention regarding all the model-related translations, as shown in the example:

entity:
  User:
    singular: User
    plural: Users
    fields:
      username: Login
      password: Password
      passwordRepeat: Repeat password
      email: Email
      posts: Posts
      postsCount: Posts count
      role: Role
      roleName: Role
      rememberMe: Remember me
    actions:
      testEmail:
        name: Test email
        subject: Test email
        content: Test email sent from demo application of the <a href="https://micrus.avris.it">Micrus</a> framework
        success: Email sent
        failure: Error while sending an email
      register: Register
      login: Log in
      logout: Log out
      myAccount: My account
    custom:
      agreement: I agree to terms and conditions of...
      wrongCredentials: Incorrect credentials
      usernameTaken: Selected login is already taken
      usernameRegex: Login can only contain letters and numbers
      passwordMismatch: Given passwords don't match

Dependency Injection of Services

Services are classes that are supposed to be shared between the application modules (controllers, views, other services, etc.), that do a specific job (like handling event or sending emails), and that generally (but necessarily) have at most one instance in the system.

They depend on one another. For instance, securityManager, responsible i.a. for logging users in and out, requires crypt that hashes passwords for it. Of course, securityManager and every other service that uses crypt, might all just create their own instances of Crypt. But what if you wanted to change the hashing algorithm? You cannot just edit the code of Avris\Micrus\Tool\Crypt (unless you want to repeat that for every instance of your application after every update of your vendors), and it's not a good idea either to change each and every of those services to start using your new implementation of the crypting tool.

Dependency Injection is a great idea to solve such problems. With DI, there is just one element, called Container, that manages all the services and dependencies between them, while the services only receive them. In that case, you'd just have to tell the Container, that from now on crypt should mean an instance of your implementation, instead of the default Avris\Micrus\Tool\Crypt.

To register a service (or overwrite one declared by Micrus or other library), you must add it to app/Config/services.yml and create a class for it (most often in App\Service namespace).

mailer:
  class: App\Service\Mailer
  parameters: [@config.parameters.mailer, @?logger]
  tags: [defaultParameters]

responseListener:
  class: App\Service\ResponseListener
  parameters: [@logger]
  events: [response]

waveTwigExtension:
  class: App\Service\WaveTwigExtension
  tags: [twigExtension]

countedProduct:
  class: App\Service\CountedProduct

countedProductFactory:
  class: App\Service\CountedProduct
  factory: true

assertLocaleSet: ~

We declared five services here. Only class field is required. A parameter can be:

Service assertLocaleSet is set by default by the framework, but you can unset it with ~.

You can also interpolate stuff using the syntax: {@rootDir}/run/images. If a service is not required, you can specify that using a question mark: @?name.

The parameters are injected to the service by the constructor:

<?php
namespace App\Service;

use App\Test\MockPHPMailer;
use Avris\Micrus\ParameterBag;
use Avris\Micrus\Tool\ParametersProvider;
use Avris\Micrus\Tool\Logger;

class Mailer implements ParametersProvider
{
    /** @var array */
    protected $options;

    /** @var Logger */
    protected $logger;

    public function __construct($options, Logger $logger = null)
    {
        $this->options = $options;
        $this->logger = $logger;
    }

    public function send($subject, $content, array $receivers)
    {
        // do the sending
        if ($this->logger) {
            $this->logger->info(l('mailer.sent', [%count% => count($reveivers)]);
        }
    }

}

To send an email:

/** @var Mailer $mailer */
$mailer = $this->getService('mailer');
$mailer->send($subject, $content, ['test@example.com' => 'Tester'])

Direct access to the Container

Container itself is a service as well, so it can be accessed with $this->getService('container') (in the controller) or injected as @container.

You can manually register and retrieve services, using its set, setArray, has, get and remove methods.

Note that the service can be anything but an array! If you set an array, it will be understood as a service configuration yet to be resolved.

Events

Services can listen to the events, such as request, response, or clearCache. Just before handling the request, Micrus notifies the Container to trigger request event. Container executes then the requestEvent method of every service that is registered with this event. The same happens for all the other event types at different times of application lifecycle.

Let's take a look at an example of service that listens to request event:

<?php
namespace App\Service;

use Avris\Micrus\Controller\Http\ResponseEvent;
use Avris\Micrus\Tool\Logger;

class ResponseListener
{
    /** @var Logger $logger */
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function responseEvent(ResponseEvent $event)
    {
        $this->logger->log('info', 'Response length: ' . strlen($event->getResponse()->getContent()));
    }
}

To create your own event, just extend Avris\Micrus\Event:

class MyOwnEvent extends Event
{
    private $param;

    public function __construct($param)
    {
        $this->param = $param;
    }

    public function getName()
    {
        return 'myown';
    }

    public function getParam()
    {
        return $this->param;
    }
}

And to trigger it:

$this->getService('container')->triggerEvent(new MyOwnEvent('foobar'));

Now all the services that listen to myown will have their myownEvent method executed.

Note that if you need a service to be run as the first one in chain when triggering an event, put an ! before the event name, eg.:

foo:
    class: App\Service\Foo
    events: ['!request', 'someOtherEvent']

Tags

Services can be grouped by tags. Other ones might then retrieve all of them at once and use them as they want. For instance, if you're using Twig, then every time it initializes, it will get injected with #twigExtension and register all of the classes it gets as Twig extensions.

Factory vs. singleton

By default services are created as singletons, which means there's always just once instance of them (well, at most one, since a service isn't initialized unless it's needed). Why waste memory on dozens of instances of a mailer, if they are going to be exactly the same? Plus, in most cases you actually need the same instance of a service used by different services.

But sometimes it's the opposite: you may need a new instance of some class every time you inject it somewhere. For this feature, just specify factory: true in the service declaration.

Resolvers

Let's say your website consists of multiple games, each on a different domain, but running by the same code. You want to have a game service that stores the currently open game. It's a more complicated logic than just putting plain data to DI or creating a class by classname and parameters. In this case, declare a resolver service:

game:
    class: App\Service\GameResolver
    parameters: ['@request', '@orm.entityManager']

And make it implement Avris\Micrus\Tool\Resolver interface. Whatever you return in its resolve method, will be put to DI as game service.

<?php
namespace App\Service;

use Avris\Micrus\Tool\Resolver;
use Avris\Micrus\Controller\Http\Request;
use Doctrine\ORM\EntityManager;

class GameResolver implements Resolver
{
    protected $request;

    protected $em;

    public function __construct(Request $request, EntityManager $em)
    {
        $this->request = $request;
        $this->em = $em;
    }

    public function resolve()
    {
        return $this->em->getRepository('game')->findOneByDomain($request->getServer('SERVER_NAME'));
    }
}

Security

Micrus handles both user's authentication (who are you?) and authorisation (do you have access to this page?).

Three most important security-related services are UserProvider, Crypt and SecurityManager. UserProvider finds a user by an identifier (email, username, ...) in any storage (config file, database, ...). Crypt takes care of hashing & validating passwords and encrypting & decrypting data. SecurityManager makes sure that only authorised : of logging the user in or out, checking their credentials etc.

In Micrus's approach, the user doesn't just have one password, but might have multiple "Authenticators". Those could be passwords, as well as "remember me" tokens stored in cookies or access tokens from OAuth, Facebook, Google+ etc.

You should create class App\Model\User that implements interface Avris\Micrus\Model\User\UserInterface and App\Model\Authenticator that implements interface Avris\Micrus\Model\User\AuthenticatorInterface.

Authorisation restrictions

Sample config/security.yml:

security:
  loginPath:   userLogin     # default: login
  afterLogout: userLogout    # default: home
  rememberMeForDays: 7       # default: 30
  restrictions:
    - { pattern: ^/admin, roles: [ROLE_ADMIN] }
    - { pattern: ^/post/add$ } # login required, but not any particular role
    - { pattern: ^/post/(\d+)/edit$, check: edit, object: '$post' }
    - { controller: App\Controller\Secret, roles: [ROLE_SECRET_KNOWER] }
    - { controller: App\Controller\Foobar/secret, roles: [ROLE_SECRET_KNOWER] }

Security restrictions can also be defined using Annotations.

Before every request, the SecurityManager checks if the request matches any of the restrictions. If it doesn't, the app flow simply continues to the controller. But if a restriction is met:

The roles condition checks if $user->getRoles() has at least one of those specified by roles.

The check condition checks for more complex conditions, like "post can only be edited by its author". To create such a check, implement GuardInterface. Your Guard will be invoked with:

Public paths

To declare some paths as public, regardless of other restrictions, just use public option. For instance of you want all paths except /login to require being logged in, use such configuration:

security:
  loginPath:   login
  afterLogout: login
  public:
   - { pattern: ^/login }
  restrictions:
    - { pattern: ^/ }

Role hierarchy

If you want all managers to automatically have all the permissions of normal users, and all admins to automatically have all the permissions of normal users and of managers, then configure those roles like this:

security:
  roles:
    ROLE_ADMIN: [ROLE_USER, ROLE_MANAGER]
    ROLE_MANAGER: [ROLE_USER]

Security enhancements

security:
  preventSessionFixation: true
  cookiesOnlyHttp: true
  encryptCookies: true
  ssl: true
  secureHeaders:
    enabled: true
    # check Avris\Micrus\Tool\Security\SecurityEnhancer::$headers for the list of those headers

UserProviders

Service UserProviderInterface is used to retrieve user data from whatever source. By default it's an instance of Avris\Micrus\Model\MemoryUserProvider, which is read-only and takes the list of users from @config.security.users. Such list should be defined in the following format:

security:
  users:
    admin:
      roles: [ROLE_ADMIN]
      authenticators:
        password: 7282d579f3c1651f44065bdde0a801d0f05e110d2e4d835274e5429d96b75679bfb815021dd8b0e4
    user:
      roles: [ROLE_USER]
      authenticators:
        password: 7a2e81a01de38cfc51d6cf5afea59de2a11d031769974ef33536c21161ad66191fcd1bfec436326c

Note that you can generate password hashes using php bin/micrus security:password:hash.

If you have an ORM module installed, it should automatically overwrite UserProviderInterface to it's own, which will retrieve user entity straight from the database.

Services

Dependency Injection

Services are classes that are supposed to be shared between the application modules (controllers, views, other services, etc.), that do a specific job (like handling event or sending emails), and that generally (but necessarily) have at most one instance in the system.

They depend on one another. For instance, securityManager, responsible i.a. for logging users in and out, requires crypt that hashes passwords for it. Of course, securityManager and every other service that uses crypt, might all just create their own instances of Crypt. But what if you wanted to change the hashing algorithm? You cannot just edit the code of Avris\Micrus\Tool\Security\Crypt (unless you want to repeat that for every instance of your application after every update of your vendors), and on the other hand it's not a good idea either to change each and every of those services to start using your new implementation of the encrypting tool.

Dependency Injection is a great idea to solve such problems. With DI, there is just one element, called Container, that manages all the services and dependencies between them, while the services only receive them. In that case, you'd just have to tell the Container, that from now on crypt should mean an instance of your implementation, instead of the default Avris\Micrus\Tool\Security\Crypt. Container will automatically inject it to all the servers that need it.

To register a service (or overwrite one declared by Micrus or other library), you must add it to app/Config/services.yml and create a class for it (most often in App\Service namespace).

mailer:
  class: App\Service\Mailer
  params: [@config.parameters.?mailer]
  calls:
    setLogger: [@?logger]
  tags: [defaultParameters]

responseListener:
  class: App\Service\ResponseListener
  params: [@logger]
  events: [response]

waveTwigExtension:
  class: App\Service\WaveTwigExtension
  tags: [twigExtension]

countedProduct:
  class: App\Service\CountedProduct

countedProductFactory:
  class: App\Service\CountedProduct
  factory: true

We declared five services here. Only class field is required. A param can be:

Those params are injected to the service by its constructor (and possibly also method calls), for example:

<?php
namespace App\Service;

use App\Test\MockPHPMailer;
use Avris\Bag\Bag;
use Avris\Micrus\Tool\Config\ParametersProvider;
use Avris\Micrus\Tool\Logger;

class Mailer implements ParametersProvider
{
    /** @var Bag */
    protected $options;

    /** @var Logger */
    protected $logger;

    public function __construct(Bag $options)
    {
        $this->options = $options;
        $this->logger = $logger;
    }

    public function setLogger(Logger $logger = null)
    {
        $this->logger = $logger;

        return $this;
    }

    public function send($subject, $content, array $receivers)
    {
        // do the sending...

        if ($this->logger) {
            $this->logger->info(count($reveivers) . ' emails sent');
        }
    }

}

Now to send an email from a controller, do this:

/** @var Mailer $mailer */
$mailer = $this->getService('mailer');
$mailer->send($subject, $content, ['test@example.com' => 'Tester'])

If a service is not required, you can specify that using a question mark: @?name.

You can also interpolate stuff into params using the syntax: {@rootDir}/run/images.

To put in the container a string value depending on other services, use resolve option:

picDir:
  resolve: '{@rootDir}/run/pic'

If you want to remove a service that has been set by default by the framework, or by a module, set it to null:

serviceName: ~

Direct access to the Container

Container itself is a service as well, so it can be injected as @container (or just @). In a controller you can also accessed it with $this->controller.

You can manually register and retrieve services, using its set, has, get and remove methods.

Note that the service can be anything but an array! If you set an array, it will be understood as a service configuration yet to be resolved.

Resolvers

Let's say your website consists of multiple games, each on a different domain, but running by the same code. You want to have a game service that stores the currently open game. It's a more complicated logic than just putting plain data to the Container or creating a class by classname and parameters. In this case, declare a resolver service:

game:
    class: App\Service\GameResolver
    params: ['@request', '@orm.entityManager']

And make it implement Avris\Micrus\Tool\Bootstrap\Resolver interface. Whatever you return in its resolve method, will be put to DI as game service.

<?php
namespace App\Service;

use Avris\Micrus\Tool\Bootstrap\Resolver;
use Avris\Micrus\Controller\Http\RequestInterface;
use Doctrine\ORM\EntityManager;

class GameResolver implements Resolver
{
    protected $request;

    protected $em;

    public function __construct(RequestInterface $request, EntityManager $em)
    {
        $this->request = $request;
        $this->em = $em;
    }

    public function resolve()
    {
        return $this->em->getRepository('game')->findOneByDomain($request->getServer('SERVER_NAME'));
    }
}

Tags

Services can be grouped by tags. Other ones might then retrieve all of them at once and use them as they want. For instance, if you're using Twig, then every time it initializes, it will get injected with #twigExtension and register all of the classes it gets as Twig extensions.

Factory vs. singleton

By default services are created as singletons, which means there's always just once instance of them (well, at most one, since a service isn't initialized unless it's needed). Why waste memory on dozens of instances of a mailer, if they are going to be exactly the same? Plus, in most cases you actually need the same instance of a service used by different services.

But sometimes it's the opposite: you may need a new instance of some class every time you inject it somewhere. For this feature, just specify factory: true in the service declaration.

Event Dispatcher

Services can listen to the events, such as request, response, or clearCache. Just before handling the request, Micrus notifies the Event Dispatcher to trigger request event. Dispatcher executes then the onRequest method of every service that is registered with this event. The same happens for all the other event types at different times of application lifecycle.

You can declare event listeners either in the config:

serviceName:
    class: Namespace\Class
    events: [eventName, anotherEvent:7]

or attach them yourself:

$this->getService('dispatcher')->attach('eventName', 'serviceName', 7);

The 7 in those examples is the priority of a given listener (default 0).

Let's take a look at an example of service that listens to response event:

<?php
namespace App\Service;

use Avris\Micrus\Controller\Http\ResponseEvent;
use Avris\Micrus\Tool\Logger;

class ResponseListener
{
    /** @var Logger $logger */
    protected $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    public function onResponse(ResponseEvent $event)
    {
        $this->logger->log('info', 'Response length: ' . strlen($event->getResponse()->getContent()));
    }
}

To create your own event, just extend Avris\Micrus\Tool\Bootstrap\Event:

class MyOwnEvent extends Event
{
    private $param;

    public function __construct($param)
    {
        $this->param = $param;
    }

    public function getName()
    {
        return 'myown';
    }

    public function getParam()
    {
        return $this->param;
    }
}

And to trigger it:

$this->getService('dispatcher')->trigger(new MyOwnEvent('foobar'));

Now all the services that listen to myown will have their myownEvent method executed.

Security

Micrus handles both user's authentication (who are you?) and authorisation (do you have access to this page?).

There are two security-related services: Crypt and SecurityManager. The first one takes care of hashing, encrypting and decrypting, while the latter: of logging the user in or out, checking their credentials etc.

In Micrus's approach, the user doesn't just have one password, but might have multiple "Authenticators". They could be passwords, as well as "remember me" tokens stored in cookies or access tokens from OAuth, Facebook, Google+ etc.

You should create class App\Model\User that implements interface Avris\Micrus\Model\UserInterface and App\Model\Authenticator that implements interface Avris\Micrus\Model\AuthenticatorInterface.

Authorization restrictions

Sample security config:

security:
  loginPath:   login     # default: login
  afterLogout: home      # default: home
  rememberMeForDays: 7   # default: 14
  restrictions:
    - { pattern: ^/admin, roles: [ROLE_ADMIN] }
    - { pattern: ^/post/add } # login required, but not any particular role
    - { pattern: ^/post/(\d+)/edit, check: canEditPost }
    - { controller: secret, roles: [ROLE_SECRET_KNOWER] }
    - { controller: foobar/secret, roles: [ROLE_SECRET_KNOWER] }

Before every request, the SecurityManager checks if its URL matches any of the restrictions. If not, it continues to the controller. Otherwise:

Additionally, SecurityManager can authorise user based on more complicated condition (e.g. post can only be edited by its author), by invoking $user's method as specified in check (with $matches as parameter):

// App\Model\User

public function canEditPost(Post $requestedPost)
{
    if ($this->isAdmin()) {
        return true;
    }

    foreach ($this->getPosts() as $postOfUser) {
        if ($postOfUser == $requestedPost) {
            return true;
        }
    }

    return false;
}

Public paths

To declare some paths as public, regardless of other restrictions, just use public option. For instance of you want all paths except /login to require being logged in, use such configuration:

security:
  loginPath:   login
  afterLogout: login
  public:
   - { pattern: ^/login }
  restrictions:
    - { pattern: ^/, roles: [ROLE_USER, ROLE_ADMIN] }

Role hierarchy

If you want all managers to automatically have all the permissions of normal users, and all admins to automatically have all the permissions of normal users and of managers, then configure those roles like this:

security:
  roles:
    ROLE_ADMIN: [ROLE_USER, ROLE_MANAGER]
    ROLE_MANAGER: [ROLE_USER]

Security enhancements

security:
  preventSessionFixation: true
  cookiesOnlyHttp: true
  encryptCookies: true
  ssl: true
  secureHeaders:
    enabled: true
    # check Avris\Micrus\Tool\Security\SecurityEnhancer::$headers for the list of those headers

UserProviders

Service userProvider is used to retrieve user data from whatever source. By default it's an instance of Avris\Micrus\Model\MemoryUserProvider, which is read-only and takes the list of users from @config.security.users. Such list should be defined in the following format:

security:
  users:
    admin:
      roles: [ROLE_ADMIN]
      authenticators:
        password: 7282d579f3c1651f44065bdde0a801d0f05e110d2e4d835274e5429d96b75679bfb815021dd8b0e4
    user:
      roles: [ROLE_USER]
      authenticators:
        password: 7a2e81a01de38cfc51d6cf5afea59de2a11d031769974ef33536c21161ad66191fcd1bfec436326c

Note that you can generate password hashes using php bin/micrus security:hash.

If you have an ORM module installed, it should automatically overwrite userProvider to it's own, which will retrieve user entity straight from the database.

Testing

Micrus recommends using PHPUnit for both unit and functional tests. They should be put in the tests directory and should keep the same namespace structure as in src folder.

For the functional tests, Micrus provides a wrapper for Symfony BrowserKit and uses Symfony DomCrawler. So the usage is quite similar to what's presented in the Symfony documentation.

Remember to explicitly include dependencies:

composer require --dev symfony/browser-kit symfony/css-selector

To use it, you need to initialise the app for tests in your tests/_bootstrap.php file:

$app = new \App\App('test', true);
\Avris\Micrus\Tool\Test\WebTestCase::init($app, __DIR__ . '/../tests/_output/fail'); 

The second argument is the directory where the HTTP responses of the failed tests will be saved for the purposes of debugging.

Your functional test cases have to extend Avris\Micrus\Tool\Test\WebTestCase. A client will be created for the whole test suite so that it can be shared between tests. It is accessible via static::$client.

You should create one story per class.

You can access the container via static::$client->getContainer().

For performance sake consider running your functional tests on a in-memory SQLite database or encapsulating your stories in a transaction.

For examples of functional and unit tests, please turn to the Micrus Demo Project.

Tools

Cacher

To boost performance in the production environment, Micrus caches config files, locales, Twig views and other data. The cache service uses the folder run/cache/{env} to save cache, where {env} is current environment. It is PSR-6 compliant, which means you can easily switch it to any other caching method, (Redis, Memcached etc.) as long as it also sticks to that standard.

There is also a cacher service, that doesn't care for the actual storing of data (it uses cache for that), but just administrates, whether and how caching should be used. It can work in three modes:

To switch a mode, just set the parameter cacherMode in app/Config/parameters.yml.

You can use cacher yourself:

$mydata = $this->getService('cacher')->cache('mydata', function($parameter) {
    // load data somehow...
    return $data;
}, [$parameter]);

It's that simple! Instead of running yourself the method which that loads your data, let Cacher wrap around it, and tell it what key to use. It will do its job, depending on its mode, and return the result.

If your service uses caching, you may consider implementing the listeners to events cacheClear and cacheWarmup. They are triggered in, respectively: php bin/micrus cache:clear and php bin/micrus cache:warmup. Note: the first one clears cache for all enviroments (to specifiy just one use php bin/micrus cache:clear -c --env=prod) and the latter runs in the default environment, cli (to change it, run php bin/micrus cache:warmup --env=prod).

You can automatically cache the services in your DI container by setting cached: true:

service_name:
    class: Acme\Service\Class
    cached: true

Logger

The logger service extends a great library, Monolog, which is PSR-3 compliant, so you can switch it to any other that is as well. For reference, please turn to Monolog's documentation.

If you don't specify a filename, default one will be used: simply {env}.log. You can configure the Logger with the following fields:

logger:
  file: ~
  round: ~
  requestIdLength: ~

The round entry can be either hour, day, week, month or year, and it means Logger will create separate log file for every given period of time.

Each log entry has a column for request ID. By default it's 4-character string, and it can be customized with requestIdLength.

Testing

Micrus is ready for use with PHPUnit, for both unit and functional tests. Please put them in tests directory and keep the same namespace structure as in app folder.

For the functional tests, Micrus uses Symfony BrowserKit together with Symfony DomCrawler, so the usage is quite similar to what's presented in the Symfony documentation.

Remember to explicitly include dependencies:

composer require --dev symfony/browser-kit symfony/css-selector

Main difference is that your functional test cases should extend Avris\Micrus\Tool\Test\WebTestCase. A client will be created for the whole test class (accessible via static::$client) so that it can be shared between tests, creating one story per class. You can access the container via static::$client->getContainer(). Also, consider encapsulating your stories in transactions to improve performance and to keep database clean.

For examples of functional and unit tests, please turn to the Micrus Demo Project.

Extending Micrus with Modules

All the dependencies are handled by Composer.

Some libraries, called "Modules", are meant specifically for Micrus: they provide templates, services, extend routing etc.

File app/Config/modules.yml contains a list of registered Modules and is loaded before any other config. Module definition must implement the Avris\Micrus\Tool\Bootstrap\Module interface, which only requires one method: extendConfig($env, $rootDir). The arrays returned by all the configs will be merged with the config of your app. The following hierarchy is kept in case one setting overwrites another:

modules < config.yml < imports < config_env.yml

Most of the times Module's config just extends the list of services. Sometimes they also add a new config namespace and declare the default values of what's inside. Take a look at the following example:

<?php
namespace Avris\Micrus\Assetic;

use Avris\Micrus\Bootstrap\Module;

class AsseticModule implements Module
{
    public function extendConfig($env, $rootDir)
    {
        return [
            'services' => [
                'asseticManager' => [
                    'class' => AsseticManager::class,
                    'params' => ['@asseticGenerator', '#asseticExtension', '{@rootDir}/web', '@cacher', '@env'],
                    'events' => ['cacheClear', 'cacheWarmup'],
                ],
                'appAsseticExtension' => [
                    'class' => AppAsseticExtension::class,
                    'params' => ['{@rootDir}/app/Asset', '@config.assetic'],
                    'tags' => ['asseticExtension'],
                ],
                'asseticGenerator' => [
                    'class' => Generator::class,
                ],
                'asseticTwigGlobals' => [
                    'class' => TwigGlobals::class,
                    'params' => ['@asseticManager'],
                    'tags' => ['twigExtension'],
                ],
            ],
            'assetic' => [
                'filters' => [],
                'assets' => [],
                'statics' => [],
            ],
        ];
    }
}

Creating Modules looks almost the same as creating normal app components, except the config isn't kept in .yml files, but in the Module class. If you want to add some templates, locales, console tasks or routes to your module, you will have to specify the directories, where Micrus can find them. Just use the tags: templateDirs, localeSet, task and routingExtension respectively.

You can create controllers inside modules, you'll just have to specify its full namespace in the routing (controller: MyModule\Controllers\FooController). But sometimes you might want a service to work as a controller (as it is for instance in the localizator service from the Localization Module: it handles the localization, but also provides a single, small action just to switch current locale).

In that case declare route's controller as: @serviceName/foobarAction. When that route is matched with the request, service serviceName will be retrieved from the container and its foobarAction method will be executed.

Since your service is not an instance of Avris\Micrus\Controller\Controller, it doesn't have direct access to them Container or Request objects. But you can inject them straight to the action. Just declare its first argument to be of an expected class, for instance:

public function switchLocaleForUserAction(RequestInterface $request, User $user, $localeName) { ... }