Micrus Tiny, yet full-stack, PHP framework

How big does a framework need to be to provide you with a quick, easy and comfortable way of creating neatly structured MVC websites? That can easily be extended and configured?

Well, not big at all. Just try Micrus! Its goal is to keep it as simple as possible, while offering all the most important features, as listed here:

Documentation

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

Instalation

Starting a project based on Micrus is simple. Just install Composer and run:

composer create-project avris/micrus-starter my_new_project

Composer will download the project with all of its dependencies. It will ask you for server parameters (like database connection) and will generate a secret encryption key unique for your app.

If there are some fixtures available in the project, you can load them with:

bin/micrus db:fixtures

And that's it. If your server is configured correctly, your app should be available under the URL:

http://localhost/my_new_project/web/app_dev.php

You might also want to check out the demo project:

composer create-project avris/micrus-demo

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. Furthermore, Micrus is simple enough, so that investigating its source code might sometimes be easier and quicker way of solving problems than 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 enough, while 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. It has a routing table that binds specific URLs to specific controllers. It says that the URL POST /post/14/edit should be handled by the action editAction inside the controller PostController. But before executing the controller, Micrus 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.

Directory structure

- app
   - Config
     - config.yml
     - config_dev.yml
     - modules.yml
     - parameters.yml
   - Controller
   - Form
   - Locale
   - Model
   - Service
   - Task
   - View
     - layout.html.twig
- bin
  - micrus
- run
  - cache
  - logs
- tests
- vendor
- web
   - app.php
   - app_dev.php

The web 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 controllers. Front controllers are tiny php files – "gateways" to all the rest of the backend code, which is hidden in different directories, so in case of server misconfiguration it doesn't leak. By default there are two front controllers in the project:

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

The run 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 app dir.

Directory bin will contain the console executables, like PHPMD, PHPUnit, and most notably: bin/micrus that can execute all the Micrus-related tasks, like clearing cache of loading fixtures. This also includes your own custom commands.

app is the directory where all the backend code for your application is located. It is organised according to the MVC pattern, which keeps different layers of an application nicely separated:

Routing and controllers

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

home:
  pattern: /
  target: Home/home

helloUser:
  pattern: /hello/{id}
  target: Home/helloUser
  requirements: { id: \d+ }

helloName:
  pattern: /hello/{name}
  target: Home/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 Home/home (that means: class App\Controller\HomeController, method homeAction). The target might also be a service from the Container: @serviceName/method.

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:

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

public function helloNameAction($name)
{
    return $this->render(['name' => $name]);
}

It receives $name from the URL and simply sends it to the view to be rendered.

All the parameters that router has found in a route can be automatically tranformed into model. You just have to specify the class of a parameter in the action's header, and it will get fetched from the database:

public function helloUserAction(User $user)
{
    return $this->render(['name' => $user->getUsername()]);
}

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

Every controller must return an object implementing Avris\Micrus\Controller\Http\ResponseInterface. For instance for JSON it would be:

return new Response(json_encode($vars), 200, ['content-type' => 'application/json']);

where 200 is a response code, and content-type is a header. You might as well use this shortcut:

return $this->renderJson($vars);

There is a shortcurt for a "regular" HTML response as well, of course:

return $this->render(['variable' => $value]);

This array might contain a key _view with a identifier (filename) of the template that should be rendered. If it's not set, it defauls to {controller name}/{action name}. You don't have to specify the extension of a template, if it's the default extension of your template engine (.html.twig for Twig, .phtml for plain PHP, etc.) So for instance our helloUserAction would render the file Hello/helloUser.html.twig, if no other is specified.

Third kind of most "popular" responses is the redirect.

return $this->redirect($url);

The $url might be just a plain URL, or you can generate it using the router (yup, the router works both ways).

return $this->redirect($this->generateUrl('helloName', ['name' => 'Jessica']);

Or with a shortcut:

return $this->redirectToRoute('helloName', ['name' => 'Jessica']);

From the controller you can access main components of the app, via: get (service), trigger (event), getRouter, getUser, getRootDir, getEm (entity manager of your ORM), getParameter, isGranted, getRequest, getQuery, getData, getSession, getFiles, getCookies, setCookie. They all do what they promise to. Please note, that the superglobal variables (well, globals too) shouldn't be ever used in the project. They are all accessible in the wrappers. DO: $this->getData()->get('title') or shorter $this->getData('title'), DON'T: $_POST['title']. Btw, with the wrappers you don't need to check for isset – if the key is not in the array, null will be returned (see: Avris Bag).

You can also create a flash message using $this->addFlash($type, $message). They are tiny messages for the user, such as Object created successfuly!, that are set before a redirect, live for just one more request, are displayed once and then forgotten.

By default they are deferred for the next request (set flash, redirect somewhere, show flash there), but adding false as the last parameter makes them shown right away.

In the view they can be rendered like this:

{% for flash in app.flashBag.all %}
    <div class="alert alert-{{ flash.type }}">
        <p>{{ flash.message }}</p>
    </div>
{% endfor %}

Model

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

All the Model classes should to be put in the App\Model namespace.

Automatcher

The automatcher service translates string values from the URL ("tags", like value of id in the route /posts/{id}/edit) into other values, depending on what a specific callable expects.

Automatcher is used to pass more meaningful data to controllers and to security check methods.

With its help our controllers don't have to look like that:

public function editAction($postId)
{
    $request = $this->get('request');
    $post = $this->getEm()->getRepository(Post::class)->find($postId);
    if (!$post) {
        throw new NotFoundException;
    }
    // ...
}

instead, they can look like that:

public function editAction(Request $request, Post $post)
{
    // ...
}

Automatcher first fills up all the parameters from the beginning that implement any interface from its internal list. By default it contains ContainerInterface and RequestInterface, but you can extend this list by defining a service that has a tag automatcherInterfacesMap and implements Avris\Micrus\Model\AutomatcherInterfacesMap.

For the rest of parameters, automatcher works like this: if a scalar is expected, value is just passed on, and if it's an object, automatcher fetches it from ORM (and throws NotFoundException if it's not there). You can overwrite that behaviour for specific tags by defining a service that has a tag automatcherSpecialTags and implements Avris\Micrus\Model\AutomatcherSpecialTags. Tags defined that way might either be ignored (null) or applied a custom callback.

Fixtures

Fixtures are sets of data that are supposed to be loaded in dev and test environments, so that the developers and functional test have some mocked entities to work on.

Micrus provides a simple way to load them. Just create a console task extending AbstractFixturesTask, and inside its load method add some code that truncates the database and creates entities.

You can then execute that task with:

php bin/micrus db:fixtures

A simple example for the Doctrine ORM:

<?php
namespace App\Task;

use App\Model\Post;
use App\Model\Category;
use Avris\Micrus\Doctrine\DoctrineFixturesTask; // extends Avris\Micrus\Console\Task\AbstractFixturesTask;
use Symfony\Component\Console\Output\OutputInterface;

class FixturesTask extends DoctrineFixturesTask
{
    protected function load(OutputInterface $output)
    {
        $output->writeln('Truncating the database');
        $this->truncateDatabase();

        $crypt = $this->container->get('crypt');

        $output->writeln('Loading: users');

        $admin = new User();
        $admin->setUsername('admin');
        $admin->setEmail('admin@micrus.avris.it');
        $admin->setRole('ROLE_ADMIN');
        $admin->createAuthenticator(SecurityManager::AUTHENTICATOR_PASSWORD, $crypt->hash('admin'));
        $this->em->persist($admin);

        $user = new User();
        $user->setUsername('user');
        $user->setEmail('user@micrus.avris.it');
        $user->setRole('ROLE_USER');
        $user->createAuthenticator(SecurityManager::AUTHENTICATOR_PASSWORD, $crypt->hash('user'));
        $this->em->persist($user);

        $output->writeln('Loading: categories');
        $categories = array();
        foreach (['comedy','drama','tragedy','news','info','curiosity'] as $name) {
            $category = new Category();
            $category->setName($name);
            $this->em->persist($category);
            $categories[] = $category;
        }

        $this->em->flush();

        $output->writeln('Fixtures loaded');
    }
}

View

Micrus is template engine agnostic (we support plain PHP and Twig). Whichever one you're using, those Micrus-specific variables, functions and filters will be available:

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\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'));
    }
}

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(Request $request, User $user, $localeName) { ... }