Architecture

Core

The core tries to be minimal. The essence of it being various wrappers around Symfony. It provides:

Everything else uses most of this.

Modules

The GNU social Component-based architecture provides a clear distinction between what can not be changed (core), what is replaceable but must be always present (component), and what can be removed or added (plugin).

This architecture has terminology differences when compared to the one that was introduced in v2. In fact, back in v2 - as the term modules is not necessarily non-essential - we would keep a "modules" directory near "plugins", to make the intended difference between both self-evident.

Now in v3, Module is the name we give to the core system managing all the modules (as it is broad enough to include both components and plugins). N.B.: there are not modules in the same sense as there are components and plugins, the latter descend from the former.

Components

The most fundamental modules are the components. These are non-core functionality expected to be always available. Unlike the core, it can be exchanged with equivalent components.

We have components for two key reasons:

  • to make available internal higher level APIs, i.e. more abstract ways of interacting with the Core;
  • to implement all the basic/essential GNU social functionality in the very same way we would implement plugins.

Currently, GNU social has the following components:

  • Avatar
  • Posting

Design principles

  • Components are independent so do not interfere with each other;
  • Component implementations are hidden;
  • Communication is through well-defined events and interfaces (for models);
  • One component can be replaced by another if its events are maintained.

Plugins (Unix Tools Design Philosophy)

GNU social is true to the Unix-philosophy of small programs to do a small job.

  • Compact and concise input syntax, making full use of ASCII repertoire to minimise keystrokes;
  • Output format should be simple and easily usable as input for other programs;
  • Programs can be joined together in “pipes” and “scripts” to solve more complex problems;
  • Each tool originally performed a simple single function;
  • Prefer reusing existing tools with minor extension to rewriting a new tool from scratch;
  • The main user-interface software (“shell”) is a normal replaceable program without special privileges;
  • Support for automating routine tasks.

Brian W. Kernighan, Rob Pike: The Unix Programming Environment. Prentice-Hall, 1984.

For instructions on how to implement a plugin and use the core functionality check the Plugins chapter.

Dependencies

  • The Core only depends on Symfony. We wrote wrappers for all the Symfony functionality we use, making it possible to replace Symfony in the future if needed and to make it usable under our programming paradigms, philosophies and conventions. V2 tried to do this with PEAR.
  • Components only depend on the Core. The Core never depends on Components.
  • Components never have inter-dependencies.
  • Plugins can depend both on the Core and on Components.
  • A plugin may recognize other plugin existence and provide extra functionality via events.

N.B.: "depend on" and "allowing to" have different implications. A plugin can throw an event and other plugins may handle such event. On the other hand, it's wrong if:

  • two plugins are inter-dependent in order to provide all of their useful functionality - consider adding configuration to your plugin;
  • a component depends on or handles events from plugins - consider throwing an event from your component replacement and then handling it from a plugin.

This "hierarchy" makes the flow of things perceivable and predictable, that helps to maintain sanity.

GNU social Coding Style

Please comply with PSR-12 and the following standard when working on GNU social if you want your patches accepted and modules included in supported releases.

If you see code which doesn't comply with the below, please fix it :)

Programming Paradigms

GNU social is written with multiple programming paradigms in different places.

Most of GNU social code is procedural programming contained in functions whose name starts with on. Starting with "on" is making use of the Event dispatcher (onEventName). This allows for a declarative structure.

Hence, the most common function structure is the one in the following example:

public function onRainStart(array &$args): bool
{
    Util::openUmbrella();
    return true;
}

Things to note in the example above:

  • This function will be called when the event "RainStart" is dispatched, thus its declarative nature. More on that in the Events chapter.
  • We call a static function from a Util class. That's often how we use classes in GNU social. A notable exception being Entities. More on that in the Database chapter.

It's also common to have functional code snippets in the middle of otherwise entirely imperative blocks (e.g., for handling list manipulation). For this we often use the library Functional PHP.

Use of reflective programming, variable functions, and magic methods are sometimes employed in the core. These principles defy what is then adopted and recommended out of the core (components, plugins, etc.). The core is a lower level part of GNU social that carefully takes advantage of these resources. Unless contributing to the core, you most likely shouldn't use these.

PHP allows for a high level of code expression. In GNU social we have conventions for when each programming style should be adopted as well as methods for handling some common operations. Such an example is string parsing: We never chain various substring calls. We write a regex pattern and then call preg_match instead. All of this consistency highly contributes for a more readable and easier of maintaining code.

Strings

Use ' instead of " for strings, where substitutions aren't required. This is a performance issue, and prevents a lot of inconsistent coding styles. When using substitutions, use curly braces around your variables - like so:

$var = "my_var: {$my_var}";

Comments and Documentation

Comments go on the line ABOVE the code, NOT to the right of the code, unless it is very short. All functions and methods are to be documented using PhpDocumentor - https://docs.phpdoc.org/guides/

File Headers

File headers follow a consistent format, as such:

 // This file is part of GNU social - https://www.gnu.org/software/social
 //
 // GNU social is free software: you can redistribute it and/or modify
 // it under the terms of the GNU Affero General Public License as published by
 // the Free Software Foundation, either version 3 of the License, or
 // (at your option) any later version.
 //
 // GNU social is distributed in the hope that it will be useful,
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 // GNU Affero General Public License for more details.
 //
 // You should have received a copy of the GNU Affero General Public License
 // along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

 /**
  * Description of this file.
  *
  * @package   samples
  * @author    Diogo Cordeiro <diogo@fc.up.pt>
  * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  */

Please use it.

A few notes:

  • The description of the file doesn't have to be exhaustive. Rather it's meant to be a short summary of what's in this file and what it does. Try to keep it to 1-5 lines. You can get more in-depth when documenting individual functions!

  • You'll probably see files with multiple authors, this is by design - many people contributed to GNU social or its forebears! If you are modifying an existing file, APPEND your own author line, and update the copyright year if needed. Do not replace existing ones.

Paragraph spacing

Where-ever possible, try to keep the lines to 80 characters. Don't sacrifice readability for it though - if it makes more sense to have it in one longer line, and it's more easily read that way, that's fine.

With assignments, avoid breaking them down into multiple lines unless neccesary, except for enumerations and arrays.

'If' statements format

Use switch statements where many else if's are going to be used. Switch/case is faster.

 if ($var == 'example') {
     echo 'This is only an example';
 } else {
     echo 'This is not a test.  This is the real thing';
 }

Do NOT make if statements like this:

 if ($var == 'example'){ echo 'An example'; }

OR this

 if ($var == 'example')
         echo "An {$var}";

Associative arrays

Always use [] instead of array(). Associative arrays must be written in the following manner:

 $array = [
     'var' => 'value',
     'var2' => 'value2'
 ];

Note that spaces are preferred around the '=>'.

A note about shorthands

Some short hands are evil:

  • Use the long format for <?php. Do NOT use <?.
  • Use the long format for <?php echo. Do NOT use <?=.

Naming conventions

Respect PSR-12 first.

  • Classes use PascalCase (e.g. MyClass).
  • Functions/Methods use camelCase (e.g. myFunction).
  • Variables use snake_case (e.g. my_variable).

A note on variable names, etc. It must be possible to understand what is meant without necessarily seeing it in context, because the code that calls something might not always make it clear.

So if you have something like:

 $notice->post($contents);

Well I can easily tell what you're doing there because the names are straight- forward and clear.

Something like this:

 foo->bar();

Is much less clear.

Also, wherever possible, avoid ambiguous terms. For example, don't use text as a term for a variable. Call back to "contents" above.

Arrays

Even though PSR-12 doesn't specifically specify rules for array formatting, it is in the spirit of it to have every array element on a new line like is done for function and class method arguments and condition expressions, if there is more than one element. In this case, even the last element should end on a comma, to ease later element addition.

 $foo = ['first' => 'unu'];
 $bar = [
     'first'  => 'once',
     'second' => 'twice',
     'third'  => 'thrice',
 ];

Comparisons

Always use symbol based comparison operators (&&, ||) instead of text based operators (and, or) in an "if" clause as they are evaluated in different order and at different speeds. This is will prevent any confusion or strange results.

Prefer using === instead of == when possible. Version 3 started with PHP 8 uses strict typing whenever possible. Using strict comparisons takes good advantage of that.

Use English

All variables, classes, methods, functions and comments must be in English. Bad english is easier to work with than having to babelfish code to work out how it works.

Encoding

Files should be in UTF-8 encoding with UNIX line endings.

No ending tag

Files should not end with an ending php tag "?>". Any whitespace after the closing tag is sent to the browser and cause errors, so don't include them.

Nesting Functions

Avoid, if at all possible. When not possible, document the living daylights out of why you're nesting it. It's not always avoidable, but PHP 5 has a lot of obscure problems that come up with using nested functions.

If you must use a nested function, be sure to have robust error-handling. This is a must and submissions including nested functions that do not have robust error handling will be rejected and you'll be asked to add it.

Scoping

Properly enforcing scope of functions is something many PHP programmers don't do, but should.

In general:

  • Variables unique to a class should be protected and use interfacing to change them. This allows for input validation and making sure we don't have injection, especially when something's exposed to the API, that any program can use, and not all of them are going to be be safe and trusted.

  • Variables not unique to a class should be validated prior to every call, which is why it's generally not a good idea to re-use stuff across classes unless there's significant performance gains to doing so.

  • Classes should protect functions that they do not want overriden, but they should avoid protecting the constructor and destructor and related helper functions as this prevents proper inheritance.

Typecasting

PHP is a soft-typed language, it falls to us developers to make sure that we are using the proper inputs. When possible, use explicit type casting. Where it isn't, you're going to have to make sure that you check all your inputs before you pass them.

All inputs should be cast as an explicit PHP type.

Not properly typecasting is a shooting offence. Soft types let programmers get away with a lot of lazy code, but lazy code is buggy code, and frankly, we don't want it in GNU social if it's going to be buggy.

Consistent exception handling

Consistency is key to good code to begin with, but it is especially important to be consistent with how we handle errors. GNU social has a variety of built- in exception classes. Use them, wherever it's possible and appropriate, and they will do the heavy lifting for you.

Additionally, ensure you clean up any and all records and variables that need cleanup in a function using try { } finally { } even if you do not plan on catching exceptions (why wouldn't you, though? That's silly.).

If you do not call an exception handler, you must, at a minimum, record errors to the log using Log::level(message).

Ensure all possible control flows of a function have exception handling and cleanup, where appropriate. Don't leave endpoints with unhandled exceptions. Try not to leave something in an error state if it's avoidable.

Return values

All functions must return a value. Every single one. This is not optional.

If you are simply making a procedure call, for example as part of a helper function, then return boolean TRUE on success, and the exception on failure.

When returning the exception, return the whole nine yards, which is to say the actual PHP exception object, not just an error message.

All return values not the above should be type cast, and you should sanitize anything returned to ensure it fits into the cast. You might technically make an integer a string, for instance, but you should be making sure that integer SHOULD be a string, if you're returning it, and that it is a valid return value.

A vast majority of programming errors come down to not checking your inputs and outputs properly, so please try to do so as best and thoroughly as you can.

NULL, VOID and SET

On the discussion of whether to use === null vs is_null(), the literature online is diverse and divided.

Some facts to consider:

Therefore, in GNU social we would expect you to use one, or the other in function of context. This is better illustrated with two example situations:

  • If you're testing if a function returned null, then you're not testing a variable's data type, you're testing whether it evaluated to null or not. So, as you normally would with a === true or === false, we prefer that you test as === null in this situation.

  • If you're testing whether a variable is of type null, then you should use is_null($var). Just as you normally would with a is_int($var) or is_countable($var).

About nullable types, we prefer that you use the shorthand ?T instead of the full form T|null as it suggests that you're considering the possibility of not having the value of a certain variable. This is reinforced by the fact that NULL can not be a standalone type in PHP.

Exception handling

Exceptions are a control-flow mechanism. The motivation for this control-flow mechanism, was specifically separating error handling from non-error handling code. In the common case that error handling is very repetitive and bears little relevance to the main part of the logic.

The exception safety level adopted in GNU social is Strong, which makes use of commit or rollback semantics: Operations can fail, but failed operations are guaranteed to have no side effects, leaving the original values intact.

In GNU social, exceptions thrown by a function should be part of its declaration. They are part of the contract defined by its interface: This function does A, or fails because of B or C.

N.B.: An error or exception means that the function cannot achieve its advertised purpose. You should never base your logic around a function's exception behaviour.

PHP has concise ways to call a function that returns multiple values (arrays/lists) and I/O parameters so, do not be tempted to use Exceptions for the purpose. Exceptions are exceptional cases and not part of a regular flow.

Furthermore, do not use error codes, that's not how we handle errors in GNU social. E.g., if you return 42, 1337, 31337, etc. values instead of FileNotFoundException, that function can not be easily understood.

Why different exceptions?

What can your caller do when he receives an exception? It makes sense to have different exception classes, so the caller can decide whether to retry the same call, to use a different solution (e.g., use a fallback source instead), or quit.

Hierarchy

GNU social has two exception hierarchies:

  • Server exceptions: For the most part, the hierarchy beneath this class should be broad, not deep. You'll probably want to log these with a good level of detail.
  • Client exceptions: Used to inform the end user about the problem of his input. That means creating a user-friendly message. These will hardly be relevant to log.

Do not extend the PHP Exception class, always extend a derivative of one of these two root exception classes.

  • Exception (from PHP)
    • ClientException (Indicates a client request contains some sort of error. HTTP code 400.)

      • InvalidFormException (Invalid form submitted.)
      • NicknameException (Nickname empty exception.)
        • NicknameEmptyException
        • NicknameInvalidException
        • NicknameReservedException
        • NicknameTakenException
        • NicknameTooLongException
        • NicknameTooShortException
    • ServerException

      • DuplicateFoundException (Duplicate value found in DB when not expected.)
      • NoLoggedInUser (No user logged in.)
      • NotFoundException
      • TemporaryFileException (TemporaryFile errors.)
        • NoSuchFileException (No such file found.)
        • NoSuchNoteException (No such note.)

General recommendations

(Adapted from http://codebuild.blogspot.com/2012/01/15-best-practices-about-exception.html)

  • In the general case you want to keep your exceptions broad but not too broad. You only need to deepen it in situations where it is useful to the caller. For example, if there are five reasons a message might fail from client to server, you could define a ServerMessageFault exception, then define an exception class for each of those five errors beneath that. That way, the caller can just catch the superclass if it needs or wants to. Try to limit this to specific, reasonable cases.
  • Deal with errors/exceptions at the appropriate level. If lower in the call stack, awesome. Quite often, the most appropriate is a much higher level.
  • Don't manage logic with exceptions: If a control can be done with if-else statement clearly, don't use exceptions because it reduces readability and performance (e.g., null control, divide by zero control).
  • Exception names must be clear and meaningful, stating the causes of exception.
  • Catch specific exceptions instead of the top Exception class. This will bring additional performance, readability and more specific exception handling.
  • Try not to re-throw the exception because of the price. If re-throwing had been a must, re-throw the same exception instead of creating a new one. This will bring additional performance. You may add additional info in each layer to that exception.
  • Always clean up resources (opened files etc.) and perform this in "finally" blocks.
  • Don't absorb exceptions with no logging and operation. Ignoring exceptions will save that moment but will create a chaos for maintainability later.
  • Exception handling inside a loop is not recommended for most cases. Surround the loop with exception block instead.
  • Granularity is very important. One try block must exist for one basic operation. So don't put hundreds of lines in a try-catch statement.
  • Produce enough documentation for your exception definitions
  • Don't try to define all of your exception classes before they are actually used. You'll wind up re-doing most of it. When you encounter an error case while writing code, then decide how best to describe that error. Ideally, it should be expressed in the context of what the caller is trying to do.

Events and event handlers

Definitions (adapted from PSR-14)

  • Event - An Event is a message produced by an Emitter. Usually denoting a state change.
  • Listener - A Listener is any PHP callable that expects to be passed an Event. Zero or more Listeners may be passed the same Event. A Listener MAY enqueue some other asynchronous behavior if it so chooses.
  • Emitter - An Emitter is any arbitrary code that wishes to dispatch an Event. This is also known as the "calling code".
  • Dispatcher - The Dispatcher is given an Event by an Emitter. The Dispatcher is responsible for ensuring that the Event is passed to all relevant Listeners.

Pattern

We implement the Observer pattern using the Mediator pattern.

The key is that the emitter should not know what is listening to its events. The dispatcher avoids modules communicate directly but instead through a mediator. This helps the Single Responsibility principle by allowing communication to be offloaded to a class that just handles communication.

How does it work? The dispatcher, the central object of the event dispatcher system, notifies listeners of an event dispatched to it. Put another way: your code dispatches an event to the dispatcher, the dispatcher notifies all registered listeners for the event, and each listener does whatever it wants with the event.

Example 1: Adding elements to the core UI

An emitter in a core twig template

{% for block in handle_event('ViewAttachment' ~ attachment.getMimetypeMajor() | capitalize , {'attachment': attachment, 'thumbnail_parameters': thumbnail_parameters}) %}
    {{ block | raw }}
{% endfor %}

Listener

/**
 * Generates the view for attachments of type Image
 *
 * @param array $vars Input from the caller/emitter
 * @param array $res I/O parameter used to accumulate or return values from the listener to the emitter
 *
 * @return bool true if not handled or if the handling should be accumulated with other listeners,
 *              false if handled well enough and no other listeners are needed
 */
public function onViewAttachmentImage(array $vars, array &$res): bool
{
    $res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]);
    return Event::stop;
}

Some things to note about this example:

  • The parameters of the handler onViewAttachmentImage are defined by the emitter;
  • Every handler must return a bool stating what is specified in the example docblock.

Example 2: Informing the core about an handler

Event emitted in the core

Event::handle('ResizerAvailable', [&$event_map]);

Event lister in a plugin

/**
 * @param array $event_map output
 *
 * @return bool event hook
 */
public function onResizerAvailable(array &$event_map): bool
{
    $event_map['image'] = 'ResizeImagePath';
    return Event::next;
}

Database

GNU social has to store a large collection of data for rapid search and retrieval.

GNU social can use different Relational Database Management Systems, namely PostgreSQL and MariaDB.

The storage is interfaced using an Object-Relational Mapper (ORM) paradigm. As the term ORM already hints at, this allows to simplify the translation between database rows and the PHP object model.

Transactions

An EntityManager and the underlying UnitOfWork employ a strategy called transactional write-behind that delays the execution of SQL statements in order to execute them in the most efficient way and at the end of a transaction so that all write locks are quickly released. You should see Doctrine as a tool to synchronize your in-memory objects with the database in well defined units of work. Work with your objects and modify them as usual. For the most part, Doctrine ORM already takes care of proper transaction demarcation for you: All the write operations (INSERT/UPDATE/DELETE) are queued until EntityManager#flush() is invoked which wraps all of these changes in a single transaction.

Declaring an Entity

<?php
namespace Plugin\Embed\Entity;

use App\Core\Entity;
use DateTimeInterface;
class AttachmentEmbed extends Entity
{
    private int $attachment_id;
    private ?string $mimetype;
    private ?string $filename;
    private ?string $media_url;
    private \DateTimeInterface $modified;
    /* Getters and Setters */
    public static function schemaDef()
    {
        return [
            'name'   => 'attachment_embed',
            'fields' => [
                'attachment_id' => ['type' => 'int', 'not null' => true, 'description' => 'Embed for that URL/file'],
                'mimetype'      => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'],
                'filename'      => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'],
                'media_url'     => ['type' => 'text', 'description' => 'URL for this Embed resource when applicable (photo, link)'],
                'modified'      => ['type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'],
            ],
            'primary key'  => ['attachment_id'],
            'foreign keys' => [
                'attachment_embed_attachment_id_fkey' => ['attachment', ['attachment_id' => 'id']],
            ],
        ];
    }
}

Retrieving an entity

$res = self::findBy('attachment_embed', ['attachment_id' => $attachment->getId(), null, 1, null);
if (count($res) === 1) {
    $object = $res[0];
}

Deleting an Entity

$object->delete();

Creating an Entity

$embed_data['attachment_id'] = $attachment->getId();
DB::persist(Entity\AttachmentEmbed::create($embed_data));
DB::flush();

Querying the database

When the ORM isn't powerful enough to satisfy your needs, you can resort to Doctrine QueryBuilder.

Cache

In the Database chapter you've learned how GNU social allows you to store data in the database. Depending on your server's specification, the database can be a bottleneck. To mitigate that, you can make use of an in-memory data structure storage to cache previous database requests.

The caching principle you'll most often make use of in GNU social is that of locality of reference. Using it is a great way of making GNU social run quicker. GNU social supports many adapters to different storages.

Although different cache adapters provide different functionalities that could be nice to take advantage of, we had to limit our cache interface to the basic avaiable in all of them. I.e., store and delete operations.

Store

/**
 * Get the cached avatar file info associated with the given GSActor id
 *
 * Returns the avatar file's hash, mimetype, title and path.
 * Ensures exactly one cached value exists
 */
public static function getAvatarFileInfo(int $gsactor_id): array
{
    try {
        $res = GSFile::error(NoAvatarException::class,
            $gsactor_id,
            Cache::get("avatar-file-info-{$gsactor_id}",
                function () use ($gsactor_id) {
                    return DB::dql('select f.file_hash, f.mimetype, f.title ' .
                        'from App\Entity\Attachment f ' .
                        'join App\Entity\Avatar a with f.id = a.attachment_id ' .
                        'where a.gsactor_id = :gsactor_id',
                        ['gsactor_id' => $gsactor_id]);
                }));
        $res['file_path'] = \App\Entity\Avatar::getFilePathStatic($res['file_hash']);
        return $res;
    } catch (Exception $e) {
        $filepath = INSTALLDIR . '/public/assets/default-avatar.svg';
        return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null];
    }
}

Delete

Cache::delete('avatar-file-info-' . $gsactor_id);

Routes and Controllers

Routes

When GNU social receives a request, it calls a controller to generate the response. The routing configuration defines which action to run for each incoming URL.

You create routes by handling the AddRoute event.

public function onAddRoute(RouteLoader $r)
{
    $r->connect('avatar', '/{gsactor_id<\d+>}/avatar/{size<full|big|medium|small>?full}',
    [Controller\Avatar::class, 'avatar_view']);
    $r->connect('settings_avatar', '/settings/avatar',
    [Controller\Avatar::class, 'settings_avatar']);
    return Event::next;
}

The magic goes on $r->connect((string $id, string $uri_path, array|string $target, array $param_reqs = [], array $accept_headers = [], array $options = [])). Here how it works:

  • id: an identifier for your route so that you can easily refer to it later;
  • uri_path: the url to be matched, can be static or have parameters; The variable parts are wrapped in {...} and they must have a unique name;
  • target: Can be an array [Class, Method to invoke] or a string with Class to __invoke;
  • param_reqs: You can either do ['parameter_name' => 'regex'] or write the requirement inline {parameter_name<regex>};
  • accept_headers: If [] then the route will accept any HTTP Accept. If the route should only be used for certain Accept headers, then specify an array of the form: ['content type' => q-factor weighting];
  • options['format']: Response content-type;
  • options['conditions']: https://symfony.com/doc/current/routing.html#matching-expressions ;
  • options['template']: Render a twig template directly from the route.

Observations

  • The special parameter _format can be used to set the "request format" of the Request object. This is used for such things as setting the Content-Type of the response (e.g. a json format translates into a Content-Type of application/json). This does not override the options['format'] nor the HTTP Accept header information.
$r->connect(id: 'article_show', uri_path: '/articles/search.{_format}',
    target: [ArticleController::class, 'search'],
    param_reqs: ['_format' => 'html|xml']
);
  • An example of a suitable accept headers array would be:
$acceptHeaders = [
    'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0,
    'application/activity+json' => 1,
    'application/json' => 2,
    'application/ld+json' => 3
];

Controllers

A controller is a PHP function you create that reads information from the Request object and creates and returns a either a Response object or an array that merges with the route options array. The response could be an HTML page, JSON, XML, a file download, a redirect, a 404 error or anything else.

HTTP method

public /**
* @param Request $request
* @param array $vars Twig Template vars and route options
*/function onGet(Request $request, array $vars): array|Response
{
    return 
}

Forms

public function settings_avatar(Request $request): array
{
    $form = Form::create([
        ['avatar', FileType::class,     ['label' => _m('Avatar'), 'help' => _m('You can upload your personal avatar. The maximum file size is 2MB.'), 'multiple' => false, 'required' => false]],
        ['remove', CheckboxType::class, ['label' => _m('Remove avatar'), 'help' => _m('Remove your avatar and use the default one'), 'required' => false, 'value' => false]],
        ['hidden', HiddenType::class,   []],
        ['save',   SubmitType::class,   ['label' => _m('Submit')]],
    ]);

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $data       = $form->getData();
        $user       = Common::user();
        $gsactor_id = $user->getId();
        // Do things
    }

    return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
}

Templates

GNU social uses the Twig template engine. When you handle a UI-related event, you add your own twig snippets either with App\Util\Formatting::twigRenderFile or App\Util\Formatting::twigRenderString.

Example

public function onAppendRightPanelBlock(array $vars, array &$res): bool
{
    if ($vars['path'] == 'attachment_show') {
        $related_notes = DB::dql('select n from attachment_to_note an ' .
    'join note n with n.id = an.note_id ' .
    'where an.attachment_id = :attachment_id', ['attachment_id' => $vars['vars']['attachment_id']]);
        $related_tags = DB::dql('select distinct t.tag ' .
    'from attachment_to_note an join note_tag t with an.note_id = t.note_id ' .
    'where an.attachment_id = :attachment_id', ['attachment_id' => $vars['vars']['attachment_id']]);
        $res[] = Formatting::twigRenderFile('attachmentShowRelated/attachmentRelatedNotes.html.twig', ['related_notes' => $related_notes]);
        $res[] = Formatting::twigRenderFile('attachmentShowRelated/attachmentRelatedTags.html.twig', ['related_tags' => $related_tags]);
    }
    return Event::next;
}

Internationalization

$user_lang = $user->getLanguage();

App\Core\I18n\I18n::_m(string $msg) -- simple message

App\Core\I18n\I18n::_m(string $ctx, string $msg) -- message with context

App\Core\I18n\I18n::_m(string|string[] $msg, array $params) -- message

App\Core\I18n\I18n::_m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two

App\Core\I18n\I18n::isRtl($user_lang);

Logging

GNU social comes with a minimalist logger class. In conformance with the twelve-factor app methodology, it sends messages starting from the WARNING level to stderr.

The minimal log level can be changed by setting the SHELL_VERBOSITY environment variable:

SHELL_VERBOSITY valueMinimum log level
-1ERROR
1NOTICE
2INFO
3DEBUG

Log Levels

GNU social supports the logging levels described by RFC 5424.

  • DEBUG (100): Detailed debug information.

  • INFO (200): Interesting events. Examples: User logs in, SQL logs.

  • NOTICE (250): Normal but significant events.

  • WARNING (300): Exceptional occurrences that are not errors. Examples: Use of deprecated APIs, poor use of an API, undesirable things that are not necessarily wrong.

  • ERROR (400): Runtime errors that do not require immediate action but should typically be logged and monitored.

  • CRITICAL (500): Critical conditions. Example: Application component unavailable, unexpected exception.

  • ALERT (550): Action must be taken immediately. Example: Entire website down, database unavailable, etc. This should trigger the SMS alerts and wake you up.

  • EMERGENCY (600): Emergency: system is unusable.

Using

Log::level(message: string, context: array);

  • The message MUST be a string or object implementing __toString().

  • The message MAY contain placeholders in the form: {foo} where foo will be replaced by the context data in key "foo".

  • The context array can contain arbitrary data. The only assumption that can be made by implementors is that if an Exception instance is given to produce a stack trace, it MUST be in a key named "exception".

Where Logs are Stored

By default, log entries are written to the var/log/dev.log file when you’re in the dev environment. In the prod environment, logs are written to var/log/prod.log, but only during a request where an error or high-priority log entry was made (i.e. Log::error() , Log::critical(), Log::alert() or Log::emergency()).

Example usage

Log::info('hello, world.');
// Using the logging context, allowing to pass an array of data along the record:
Log::info('Adding a new user', ['username' => 'Seldaek']);

Queues and daemons

Some activities that GNU social needs to do, like broadcasting with OStatus or ActivityPub, SMS, XMPP messages and TwitterBridge operations, can be 'queued' and done by off-line bots instead.

Run the queue handler with:

php bin/console messenger:consume async --limit=10 --memory-limit=128M --time-limit=3600

GNU social uses Symfony, therefore the documentation on queues might be useful.

TODO queuing

OpportunisticQM plugin

This plugin is enabled by default. It tries its best to do background jobs during regular HTTP requests, like API or HTML pages calls.

Since queueing system is enabled by default, notices to be broadcasted will be stored, by default, into DB (table queue_item).

Whenever it has time, OpportunisticQM will try to handle some of them.

This is a good solution whether you:

  • have no access to command line (shared hosting)
  • do not want to deal with long-running PHP processes
  • run a low traffic GNU social instance

In other case, you really should consider enabling the queuedaemon for performance reasons. Background daemons are necessary anyway if you wish to use the Instant Messaging features such as communicating via XMPP.

Queue deamon

It's recommended you use the deamon, you must be able to run long-running offline processes, either on your main Web server or on another server you control. (Your other server will still need all the above prerequisites, with the exception of Apache.) Installing on a separate server is probably a good idea for high-volume sites.

  1. You'll need the "CLI" (command-line interface) version of PHP installed on whatever server you use.

    Modern PHP versions in some operating systems have disabled functions related to forking, which is required for daemons to operate. To make this work, make sure that your php-cli config (/etc/php5/cli/php.ini) does NOT have these functions listed under 'disable_functions':

    * pcntl_fork, pcntl_wait, pcntl_wifexited, pcntl_wexitstatus,
      pcntl_wifsignaled, pcntl_wtermsig
    

    Other recommended settings for optimal performance are: * mysqli.allow_persistent = On * mysqli.reconnect = On

  2. If you're using a separate server for queues, install StatusNet somewhere on the server. You don't need to worry about the .htaccess file, but make sure that your config.php file is close to, or identical to, your Web server's version.

  3. In your config.php files (on the server where you run the queue daemon), set the following variable:

    $config['queue']['daemon'] = true;
    

    You may also want to look at the 'Queues and Daemons' section in this file for more background processing options.

  4. On the queues server, run the command scripts/startdaemons.sh.

This will run the queue handlers:

  • queuedaemon.php - polls for queued items for inbox processing and pushing out to OStatus, SMS, XMPP, etc.
  • imdaemon.php - if an IM plugin is enabled (like XMPP)
  • other daemons, like TwitterBridge ones, that you may have enabled

These daemons will automatically restart in most cases of failure including memory leaks (if a memory_limit is set), but may still die or behave oddly if they lose connections to the XMPP or queue servers.

It may be a good idea to use a daemon-monitoring service, like 'monit', to check their status and keep them running.

All the daemons write their process IDs (pids) to /var/run/ by default. This can be useful for starting, stopping, and monitoring the daemons. If you are running multiple sites on the same machine, it will be necessary to avoid collisions of these PID files by setting a site- specific directory in config.php:

   $config['daemon']['piddir'] = __DIR__ . '/../run/';

It is also possible to use a STOMP server instead of our kind of hacky home-grown DB-based queue solution. This is strongly recommended for best response time, especially when using XMPP.

Developing Plugins

Adding configuration to a plugin

The trade-off between re-usability and usability

The more general the interface, the greater the re-usability, but it is then more complex and hence less usable.

Awesomeness

The Core

This documentation adopted a top-down approach. We believed this to be the most helpful as it reduces the time needed to start developing third party plugins. To contribute to GNU social's core, on the other hand, it's important to understand its flows and internals well.

The core tries to be minimal. The essence of it being various wrappers around Symfony. It is divided in:

Overview of GNU social's Core Internals

GNU social's execution begins at public/index.php, which gets called by the webserver for all requests. This is handled by the webserver itself, which translates a GET /foo to GET /index.php?p=foo. This feature is called 'fancy URLs', as it was in V2.

The index script handles all the initialization of the Symfony framework and social itself. It reads configuration from .env or any .env.*, as well as social.yaml and social.local.yaml files at the project root. The index script creates a Kernel object, which is defined in src/Kernel.php. This is the part where the code we control starts; the Kernel constructor creates the needed constants, sets the timezone to UTC and the string encoding to UTF8. The other functions in this class get called by the Symfony framework at the appropriate times. We will come back to this file.

Registering services

Next, the src/Util/GNUsocial.php class is instantiated by the Symfony framework, on the 'onKernelRequest' or 'onCommand' events. The former event, as described in the docs:

This event is dispatched very early in Symfony, before the controller is determined. It's useful to add information to the Request or return a Response early to stop the handling of the request.

The latter, is launched when the bin/console script is used.

In both cases, these events call the register function, which creates static references for the services such as logging, event and translation. This is done, so these services can be used via static function calls, which is much less verbose and more accessible than the way the framework recommends. This function also loads all the Components and Plugins, which like in V2, are modules that aren't directly connected to the core code, being used to implement internal and optional functionality respectively, by handling events launched by various parts of the code.

Database definitions

Going back to the Kernel, the build function gets called by the Symfony framework and allows us to register a 'Compiler Pass'. Specifically, we register App\DependencyInjection\Compiler\SchemaDefPass and App\DependencyInjection\Compiler\ModuleManagerPass. The former adds a new 'metadata driver' to Doctrine. The metadata driver is responsible for loading database definitions. We keep the same method as in V2, where each 'Entity' has a schemaDef static function which returns an array with the database definition. The latter handles the loading of modules (components and plugins).

This database definition is handled by the SchemaDefPass class, which extends Doctrine\Persistence\Mapping\Driver\StaticPHPDriver. The function loadMetadataForClass is called by the Symfony framework for each file in src/Entity/. It allows us to call the schemaDef function and translate the array definition to Doctrine's internal representation. The ModuleManagerPass later uses this class to load the entity definitions from each plugin.

Routing

Next, we'll look at the RouteLoader, defined in src/Core/Router/RouteLoader.php, which loads all the files from src/Routes/*.php and calls the static load method, which defines routes with an interface similar to V2's connect, except it requires an additional identifier as the first argument. This identifier is used, for instance, to generate URLs for each route. Each route connects a URL path to a Controller, with the possibility of taking arguments, which are passed to the __invoke method of the respective controller or the given method. The controllers are defined in src/Controller/ or plugins/*/Controller or components/*/Controller and are responsible for handling a request and return a Symfony Response object, or an array that gets converted to one (subject to change, in order to abstract HTML vs JSON output).

This array conversion is handled by App\Core\Controller, along with other aspects, such as firing events we use. It also handles responding with the appropriate requested format, such as HTML or JSON, with what the controller returned.

End to end

The next steps are handled by the Symfony framework which creates a Request object from the HTTP request, and then a corresponding Response is created by App\Core\Controller, which matches the appropriate route and thus calls its controller.

Performance

All of this happens on each request, which seems like a lot to handle, and would be too slow. Fortunately, Symfony has a 'compiler' which caches and optimizes the code paths. In production mode, this can be done through a command, while in development mode, it's handled on each request if the file changed, which has a performance impact, but obviously makes development easier. In addition, we cache all the module loading.

UI

Security