SOMA (Slim Open Modular Framework) is a lightweight PHP micro-framework, designed to provide the bare essentials and lay a foundation for a developer to modularly put together their application without the framework getting in the way. soma/soma is the core that provides config loading, DI container, environment loading, service providers, facades, class aliases and a command line interface.

ReadMe

Installation

Soma requires composer for dependency management

composer require soma/soma

If you want to start an entirely new project rather than integrating it into your current solution you can use soma/project as scaffolding:

composer create-project soma/project [project-directory]

The required paths need to be created by the framework before you can run your application. You can do so either by executing install() on a configured instance of Application or by running the app:install command.

php appctrl app:install

Usage

Setup

All essential configuration can be set via the Soma\Application instance or by creating a .env file in the root of your project.

APP_URL="http://localhost:8000"
APP_STAGE="development"
APP_TIMEZONE="Europe/Stockholm"
APP_DEBUG=true
APP_OPTIMIZE=false
APP_CONFIG="/absolute/path/to/your/config/folder"
APP_STORAGE="/absolute/path/to/your/storage/folder"
require __DIR__.'/../vendor/autoload.php';

use Soma\Application;

$app = Application::getInstance()
    ->setRootPath(__DIR__)
    ->setRootUrl('http://localhost:8000/')
    ->setPath('storage', '/absolute/path/to/your/storage/folder')
    ->registerConfig('/absolute/path/to/your/config/folder')
    ->init();

The storage directory needs to be writable by the application.

Using the .env file is the recommended approach (info) and it allows one to easily create a web entry-point as well as a cli with minimal setup:

index.php

require __DIR__.'/../vendor/autoload.php';

use Soma\Application;

$app = Application::getInstance();

$app->init(__DIR__);

appctrl

#!/usr/bin/env php

require __DIR__.'/vendor/autoload.php';

use Soma\Console;
use Soma\Application;

$app = Application::getInstance();
$app->init(__DIR__.'/public');

$app->getConsole()->run();

Configuration

Configuration files can be PHP files returning arrays (recommended) or JSON, YAML and INI configuration files:

config/app.php

return [
    'name' => 'My App',
    'version' => '1.0.0',
    'date-format' => '%Y-%m-%d',
    'timezone' => 'Europe/Stockholm',
    'providers' => [
        \Soma\Providers\EventsProvider::class,
        \MyApp\Providers\RoutingProvider::class,
        \MyApp\Providers\CacheProvider::class,
    ],
    'aliases' => [
        'ServiceProvider' => \Soma\ServiceProvider::class,
        'Container' => \Psr\Container\ContainerInterface::class,
        'Collection' => \Illuminate\Support\Collection::class,
        'Repository' => \Soma\Repository::class,
        'Store' => \Soma\Store::class,
        'Event' => \Symfony\Component\EventDispatcher\Event::class,
        'GenericEvent' => \Symfony\Component\EventDispatcher\GenericEvent::class,

        'App' => \Soma\Facades\App::class,
        'Config' => \Soma\Facades\Config::class,
        'Event' => \Soma\Facades\Event::class,
    ],
    'commands' => [
        \Soma\Commands\AppInstall::class,
        \Soma\Commands\AppUninstall::class,
        \Soma\Commands\AppRefresh::class,
        \Soma\Commands\AppTinker::class,
        \Soma\Commands\AppServe::class,
        \Soma\Commands\AppClearCache::class,
    ],
];

These values are then retrievable either via the Config facade or a helper function using dot-notation, namespaced by the name of the config file:

$appName = config('app.name');

Services

The service providers are how you can modularly add in functionality to your application. The ServiceProvider class has been designed to be compatible with Illuminate\Support\ServiceProvider and should be able to register them as well as long as they don't call any Laravel specific code. It's also been designed according to the now deprecated ContainerInterop standard. Unfortunately the extension definitions have the arguments reversed in SOMA for compatibility with PHP-DI, the container library. A typical ServiceProvider may look like the following:

namespace MyApp\Providers;

use Soma\Store;
use Soma\ServiceProvider;
use Psr\Container\ContainerInterface;

use MyRouter;

class RoutingProvider extends ServiceProvider
{
    public function ready(ContainerInterface $c) : void
    {
        if (! is_cli()) {
            $c->get('router')->resolveRequest();
        }
    }

    public function getFactories() : array
    {
        return [
            'router' => function(ContainerInterface $c) {
                return new MyRouter();
            },
        ];
    }

    public function getExtensions() : array
    {
        return [
            'paths' => function(Store $paths, ContainerInterface $c) {
                $paths['admin'] = realpath(__DIR__.'/../');
                $paths['admin.assets'] = $paths['admin'].'/assets');

                return $paths;
            },
            'urls' => function(Store $urls, ContainerInterface $c) {
                $urls['admin'] = $urls['root'].'/admin';

                return $urls;
            },
        ];
    }
}

All definitions from getExtensions are automatically wrapped with DI\decorate so that those changes gets applied whenever you resolve a definition from the container. However, you can use any PHP-DI definition type for both getFactories and getExtensions and the result of the latter isn't wrapped if it's already been wrapped by PHP-DI.

Check out the source code to see all ServiceProvider features.

Commands

The console engine is built on Illuminate\Console and the commands are defined in the same manner as in Laravel 7.0. For example:

namespace MyApp\Commands;

use Soma\Command;

class HelloWorld extends Command
{
    protected $signature = 'say:hello {who?}';
    protected $description = 'Say hello to the world.';

    public function handle()
    {
        $who = $this->argument('who', 'world');

        $this->info('Hello '.$who.'!');
    }
}

As long as the command has been registered it can be executed either via Application::runCommand or a console script:

php appctrl say:hello "everybody"
Predefined commands
app:tinker

The tinker command starts an interactive PHP shell within the application environment - an effective way to test code or make quick changes to data using the application API.

app:install

The install command should be run before you start coding to make sure all necessary directories have been created. By default the application creates a symbolic link to a sub-folder of the storage directory in the public directory in order to be able to serve cached or uploaded resources. Other service providers can implement the install method and do necessary filesystem changes that the normal web server user wouldn't be able to (it's recommended to leave the web root read-only for the web server).

The framework keeps track of which service providers have been installed and which hasn't, so if new services are added they can be run with the same command without risking running procedures multiple times from one provider. Any single provider can be called on its own by specifying the fully qualified class name of the provider as an argument to the command.

app:uninstall

The command is implemented the same way as install and is meant to reverse the changes made during installation.

app:refresh

If there are times one would like to make filesystem changes depending on the state of other components (for example when configuration changes) then refresh can be implemented to handle those use cases. For example a "theme" service that requires a symbolic link to the active theme's assets in the public directory.

app:serve

The command starts the internal PHP web server in the public directory to allow one to quickly set up a development environment without the need for a fully featured HTTP server.

app:clear-cache

The clear-cache commands deletes by default the generated files by Application and provides functionality to hook in other service's procedures for emptying their cache. The framework uses a PSR-14 event system internally that can be easily consumed by your own app by registering the Soma\Providers\EventsProvider service provider. Whenever the command is run the framework dispatches the event app.cache.clear which you can hook your own logic into by registering a listener (see definition for the listen helper in helpers.php).

All paths registered under the cache namespace (e.g. cache.storage) can be automatically handled without having to define your own custom logic. They are automatically emptied/removed when the command is run or if it is specifically targeted when executing the command: php appctrl app:cache-clear storage

Helpers

The file helpers.php contain a couple of functions that are meant to simplify either calling app services or work with certain types of data. There's also useful classes for working with data-sets like Soma\Store, Soma\Repository and Soma\Manifest (checkout the source). The framework also depends on illuminate\support that provide a whole bunch of helpers for you to make use of.

License

MIT

Helpers


app : mixed

Resolve a dependency from the container

Parameters
  • string|null - $abstract - If null then the \Soma\Application instance itself
function app($abstract = null)
{
    $app = \Soma\Application::getInstance();

    if (is_null($abstract)) {
        return $app;
    }

    return $app->get($abstract);
}

should_optimize : boolean

Checks whether APP_OPTIMIZE is true

function should_optimize() : bool
{
    return app()->isPerformanceMode();
}

is_debug : boolean

Checks whether APP_DEBUG is true

function is_debug() : bool
{
    return app()->isDebug();
}

is_ajax : boolean

Checks if the application is responding to an AJAX request

function is_ajax() : bool
{
    return app()->isAjaxRequest();
}

is_cli : boolean

Checks if the application is run via the command line interface

function is_cli() : bool
{
    return app()->isCommandLine();
}

is_web : boolean

Checks if the application is responding to a regular web request

function is_web() : bool
{
    return app()->isWebRequest();
}

app_stage : boolean|string

Test string against APP_STAGE

Parameters
  • $test - If - omitted then the current stage will be returned
function app_stage($test = '')
{
    if (! empty($test)) {
        return app()->isStage($test);
    }
    else {
        return app()->getStage();
    }
}

app_path : string

Get the path to the application folder or a resource relative to its root

Parameters
  • string - [$path]
function app_path($path = '') : string
{
    return app()->getRootPath($path);
}

app_url : string

Get the URL to the application folder or a resource relative to its root

Parameters
  • string - [$url]
function app_url($url = '') : string
{
    return app()->getRootUrl($url);
}

public_path : string

Get the path to the public folder or a resource relative to its root

Parameters
  • string - [$path]
function public_path($path = '') : string
{
    return app_path($path);
}

public_url : string

Get the url to the public folder or a resource relative to its root

Parameters
  • string - [$url]
function public_url($url = '') : string
{
    return app_url($url);
}

module_url : string

Get the url for a module resource or its root

The module helper is simply building an URL according what's recommended as best practice for modules to serve content.

Parameters
  • string - [$path]
  • string - [$url]
function module_url(string $module, $url = '') : string
{
    return get_url('extensions.public').'/'.$module.($url ? '/'.$url : $url);
}

storage_path : string

Get the path to the storage folder or a resource relative to its root

Parameters
  • string - [$path]
function storage_path($path = '') : string
{
    return get_path('storage').($path ? '/'.$path : $path);
}

get_path : string

Get a specific named path registered with the application

Parameters
  • string - $name
  • string|null - [$default]
function get_path($name, $default = null)
{
    return app()->paths()->get($name, $default);
}

get_url : string

Get a specific named URL registered with the application

Parameters
  • string - $name
  • string|null - [$default]
function get_url($name, $default = null)
{
    return app()->urls()->get($name, $default);
}

config : mixed

Get the specified configuration value.

Parameters
  • string - $key - A key namespaced using dot-notation
  • mixed - [$default]
function config(string $key, $default = null)
{
    return app('config')->get($key, $default);
}

event : array|null

Trigger an event

Parameters
  • string|object - $event
  • mixed - [$payload]
  • boolean - [$halt]
function event($event, $payload = [], $halt = false)
{
    return app()->getEventDispatcher()->dispatch($event, $payload, $halt);
}

listen : void

Register an event listener

Parameters
  • string|array - $events
  • mixed - $listener
function listen($events, $listener) : void
{
    app()->getEventDispatcher()->listen($events, $listener);
}

run_command : int

Call a console command

Parameters
  • string - $command
function run_command(string $command) : int
{
    return app()->runCommand($command);
}

is_valid : boolean

Attempt to determine if the object is created correctly

Classes can implement \Soma\Contracts\ValidityChecking to make use of the this feature better.

Parameters
  • mixed - $object
function is_valid($object) : bool
{
    if (($object instanceof \Soma\Contracts\ValidityChecking || method_exists($object, '__validate')) && $object->__validate()) {
        return true;
    }
    elseif (! is_null($object)) {
        return true;
    }

    return false;
}

make_datetime : \DateTime

Convert a date into a datetime

If a format isn't set then the DATE_FORMAT constant will be used, and if that isn't defined then DateTime::ISO8601 will be used as fallback.

Parameters
  • string|int - $date_str
  • string|null - [$format]
function make_datetime($date_str, $format = null)
{
    if (empty($format)) {
        if (is_int($date_str))
            $format = 'U';
        elseif (defined('DATE_FORMAT'))
            $format = DATE_FORMAT;
        else
            $format = \DateTime::ISO8601;
    }

    return \DateTime::createFromFormat($format, $date_str);
}

format_date : string

Format a \DateTime

Will use DATE_FORMAT if defined and simply guess the format if not

Parameters
  • \DateTime - \DateTime - $date
  • string|null - [$format]
function format_date($date, $format = null)
{
    if (is_null($format) && defined('DATE_FORMAT')) {
        $format = DATE_FORMAT;
    }

    if ($date instanceof \DateTime) {
        return $date->format($format);
    }

    return $date;
}

validate_date : bool

Determine if a date is valid

Will use DATE_FORMAT if defined and simply guess the format if not

Parameters
  • string - $date
  • string|null - [$format]
function validate_date($date, $format = null) : bool
{
    if (is_null($format) && defined('DATE_FORMAT')) {
        $format = DATE_FORMAT;
    }

    $d = \DateTime::createFromFormat($format, $date);
    return $d && $d->format($format) === $date;
}

empty_dir : boolean

Empty the children of a directory

Parameters
  • string - $path
  • boolean - [$recursive]
  • boolean - [$preserveDirs]
function empty_dir($path, $recursive = true, $preserveDirs = false) : bool
{
    if (! is_dir($path)) {
        return false;
    }

    foreach (glob($path.'/*') ?: [] as $file) {
        if (is_dir($file)) {
            if (! $recursive) {
                continue;
            }

            if ($preserveDirs) {
                empty_dir($file, true, true);
            } else {
                runlink($file);
            }
        } else {
            unlink($file);
        }
    }

    return true;
}

Recursive unlink

Parameters
  • string - $path

canonicalize_path : string

Will resolve "../" and "./"

An alternative to using realpath if you wish to avoid resolving symlinks

Parameters
  • string - $address
function canonicalize_path($address) : string
{
    $address = explode('/', $address);
    $keys = array_keys($address, '..');

    foreach($keys as $keypos => $key) {
        array_splice($address, $key - ($keypos * 2 + 1), 2);
    }

    $address = implode('/', $address);
    $address = str_replace('./', '', $address);

    return $address;
}

make_numeric : int|float

Convert string to a numeric

Parameters
  • string - $val
function make_numeric($val)
{
    if (is_numeric($val)) {
        return $val + 0;
    }

    return 0;
}

is_booly : boolean

Check whether a string has a "booly" value

Parameters
  • string - $val
function is_booly($val) : bool
{
    switch (strtolower($val)) {
        case "y":
        case "yes":
        case "(yes)":
        case "true":
        case "(true)":
        case "n":
        case "no":
        case "(no)":
        case "false":
        case "(false)":
            return true;
    }

    return false;
}

make_bool : bool

Convert object into its boolean value

Parameters
  • mixed - $val
function make_bool($val) : bool
{
    if (is_bool($val)) {
        return $val;
    }
    
    switch (strtolower($val)) {
        case "y":
        case "yes":
        case "(yes)":
        case "true":
        case "(true)":
            return true;
        case "n":
        case "no":
        case "(no)":
        case "false":
        case "(false)":
            return false;
    }

    return boolval($val);
}

ensure_dir_exists : string

Creates directory recursively if it doesn't exist and returns it

Parameters
  • string - $path
  • integer - $permissions
function ensure_dir_exists($path, $permissions = 0775)
{
    if (! file_exists($path)) {
        mkdir($path, $permissions, true);
    }

    return $path;
}

is_url : boolean

Checks whether a string is an URL

Parameters
  • string - $url
function is_url(string $url) : bool
{
    return (filter_var($url, FILTER_VALIDATE_URL)) ? true : false;
}

parse_attributes : string

Combines an associative array into an HTML attribute string

Parameters
  • array - $attr
function parse_attributes(array $attr = []) : string
{
    return join(' ', array_map(function($key) use ($attr) {
       if (is_bool($attr[$key])) {
          return $attr[$key] ? $key : '';
       }

       if (is_array($attr[$key])) {
           return $key.'="'.implode(' ', $attr[$key]).'"';
       }
       else {
           return $key.'="'.$attr[$key].'"';
       }
    }, array_keys($attr)));
}

common_path : string

Returns the lowest common directory of array of paths

Parameters
  • array - $paths
function common_path(array $paths) : string
{
    $lastOffset = 1;
    $common = '/';

    while (($index = strpos($paths[0], '/', $lastOffset)) !== false) {
        $dirLen = $index - $lastOffset + 1;	// include /
        $dir = substr($paths[0], $lastOffset, $dirLen);

        foreach ($paths as $path) {
            if (substr($path, $lastOffset, $dirLen) != $dir) {
                return $common;
            }
        }

        $common .= $dir;
        $lastOffset = $index + 1;
    }

    return substr($common, 0, -1);
}

remove_double_slashes : string

Remove double forward slashes from string

Parameters
  • string - $path
function remove_double_slashes(string $path) : string
{
    $path = str_replace('//', '/', $path);
    $path = str_replace('//', '/', $path);
    return $path;
}

build_url : string

Construct an URL from an array

See PHP documentation for parse_url

Parameters
  • array - $parts
function build_url(array $parts) : string
{
    return (isset($parts['scheme']) ? "{$parts['scheme']}:" : '') .
        ((isset($parts['user']) || isset($parts['host'])) ? '//' : '') .
        (isset($parts['user']) ? "{$parts['user']}" : '') .
        (isset($parts['pass']) ? ":{$parts['pass']}" : '') .
        (isset($parts['user']) ? '@' : '') .
        (isset($parts['host']) ? "{$parts['host']}" : '') .
        (isset($parts['port']) ? ":{$parts['port']}" : '') .
        (isset($parts['path']) ? "{$parts['path']}" : '') .
        (isset($parts['query']) ? "?{$parts['query']}" : '') .
        (isset($parts['fragment']) ? "#{$parts['fragment']}" : '');
}

rel_path : string

Convert an absolute path into a relative

Parameters
  • string - $path
  • string - $compareWith
function rel_path(string $path, string $compareWith) : string
{
    return ltrim(substr($path, strlen($compareWith)), '/');
}