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.
Featured
Papyrus is a flat file CMS, utilizing Markdown files and heavy use of file compilation and performance optimizations to provide a fast website that is easy to administrate. It's meant to provide simple publishing without requiring knowledge of static website compilers and avoiding their limitations.
-
nsrosenqvist/soma-router
Simple integration of League Route into SOMA
-
nsrosenqvist/soma-cache
Symfony Cache for SOMA
-
nsrosenqvist/soma-database
Illuminate Database tools integrated with SOMA
-
nsrosenqvist/soma-logger
Simple integration of Monolog with SOMA
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
- should_optimize
- is_debug
- is_ajax
- is_cli
- is_web
- app_stage
- app_path
- app_url
- public_path
- public_url
- module_url
- storage_path
- get_path
- get_url
- config
- event
- listen
- run_command
- is_valid
- make_datetime
- format_date
- validate_date
- empty_dir
- runlink
- canonicalize_path
- make_numeric
- is_booly
- make_bool
- ensure_dir_exists
- is_url
- parse_attributes
- common_path
- remove_double_slashes
- build_url
- rel_path
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
Toggle Codefunction should_optimize() : bool
{
return app()->isPerformanceMode();
}
is_debug : boolean
Checks whether APP_DEBUG is true
Toggle Codefunction is_debug() : bool
{
return app()->isDebug();
}
is_ajax : boolean
Checks if the application is responding to an AJAX request
Toggle Codefunction is_ajax() : bool
{
return app()->isAjaxRequest();
}
is_cli : boolean
Checks if the application is run via the command line interface
Toggle Codefunction is_cli() : bool
{
return app()->isCommandLine();
}
is_web : boolean
Checks if the application is responding to a regular web request
Toggle Codefunction 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;
}
runlink : bool
Recursive unlink
Parameters
- string - $path
function runlink($path) : bool
{
if (! is_dir($path)) {
return false;
}
$di = new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS);
$ri = new \RecursiveIteratorIterator($di, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($ri as $file) {
$file->isDir() ? rmdir($file) : unlink($file);
}
return rmdir($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)), '/');
}