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.
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
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 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');
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.
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"
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.
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.
The command is implemented the same way as install and is meant to reverse the changes made during installation.
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.
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.
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
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.
MIT