oasis / slimapp
slim app framework
Installs: 12 036
Dependents: 0
Suggesters: 0
Security: 0
Stars: 2
Watchers: 5
Forks: 0
Open Issues: 0
Requires
- php: >=7.0
- oasis/http: ^2.0
- oasis/logging: ^1.2.0
- oasis/utils: ^1.6
- symfony/config: ^4.0
- symfony/console: ^4.0
- symfony/dependency-injection: ^4.0
- symfony/filesystem: ^4.0
- symfony/finder: ^4.0
Requires (Dev)
- doctrine/orm: ^2.5
- oasis/aws-wrappers: ^2.2.1
- oasis/doctrine-addon: ^2.0.2
- oasis/dynamodb-odm: ^1.0
- phpunit/phpunit: ^5.7
Suggests
- ext-pcntl: *
- dev-master
- v2.6.3
- v2.6.2
- v2.6.1
- v2.6.1-alpha2
- v2.6.1-alpha1
- v2.6.0-alpha1
- v2.5.1
- v2.5.1-alpha1
- v2.5.0
- v2.4.0
- v2.2.4
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.0
- v2.0.5
- v2.0.4
- v2.0.2
- v2.0.1
- v2.0.0
- v1.12.1
- v1.12.0
- v1.11.4
- v1.11.3
- v1.11.2
- v1.11.1
- v1.11.0
- v1.10.0
- v1.9.2
- v1.9.1
- v1.9.0
- v1.8.8
- v1.8.7
- v1.8.6
- v1.8.5
- v1.8.4
- v1.8.3
- v1.8.2
- v1.8.1
- v1.8.0
- v1.7.19
- v1.7.18
- v1.7.17
- v1.7.16
- v1.7.15
- v1.7.14
- v1.7.13
- v1.7.12
- v1.7.11
- v1.7.10
- v1.7.9
- v1.7.8
- v1.7.7
- v1.7.6
- v1.7.5
- v1.7.4
- v1.7.3
- v1.7.2
- v1.7.1
- v1.7.0
- v1.6.7
- v1.6.6
- v1.6.5
- v1.6.4
- v1.6.3
- 1.6.2
- v1.6.1
- v1.6.0
- v1.5.1
- v1.5.0
- v1.4.9
- v1.4.8
- v1.4.7
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-develop
- dev-feature/migration-to-silex-2
This package is auto-updated.
Last update: 2024-10-16 21:17:38 UTC
README
The Slim Application Framework (SlimApp) is an all-in-one framework aiming to make development of PHP project, either web or console, faster and easier.
- Installation & Setup
- Directory structure
- Configuration
- Bootstrap
- Refer to Config Value
- Service Container
- Service Description
- Define the "@app" service
- Logging
- HTTP Kernel
- Command Line Interface
- The Daemon Sentinel
Installation & Setup
The framework includes a list of useful PHP components. This makes it easy when setting up a new project: you would only need to use composer to require the project itself, and then run the project setup command.
Run the following command under a new project root directory to install the project:
composer require oasis/slimapp
After installation, you may initialize your project by running:
./vendor/bin/slimapp slimapp:project:init
Follow on screen prompts to provide necessary information, and your project directory structure will be automatically created.
Below is a list of explanations about information asked during project initialization:
In the rest part of this document, we assume the project is called test-project and the vendor is minhao. All other settings will retain their default values.
Directory structure
An automatically initialized project will have a directory structure like below:
+ PROJECT_DIR/
+ assets/ # static assets directory
+ bin/ # executable directory
- test-project.php # auto-generated project CLI entry point (executable)
+ cache/ # default cache directory
+ config/ # config file direcotry
- cli-config.php # auto-generated Doctrine CLI config file
- config.yml # auto-generated configuration YAML file
- routes.yml # auto-generated routing YAML file
- services.yml # auto-generated service container (to be parsed by symfony/di)
+ src/ # default source code (class files) directory
+ Controllers/ # namespace for controller classes
- DemoController.php # as the name tells, a demo controller
+ Database/ # namespace for db classes
- TestProjectDatabase.php # class which provides access to EntityManager and DBAL connection
- TestProject.php # base class for the project, extending SlimApp class
- TestProjectConfiguration.php # configuration definition class for config/config.yml
+ templates/ # default Twig template base directory
+ vendor/ # composer components directory
+ web/ # web entry directory
- front.php # entry file for HTTP Kernel
- bootstrap.php # bootstrap file
- composer.json # composer config file
- composer.lock # composer lock file
Configuration
The fundamental configuration file for SlimApp is config.yml
and it is located under the config
directory. This is a YAML file which can be interpreted as an array in PHP. Below is the default content of an auto-generated config file:
# config.yml is_debug: true # is application in debug mode dir: # directory settings log: /data/logs/test-project # logging dir data: /data/test-project # data dir cache: /project-root/cache # cache dir template: /project-root/templates # template dir db: # database settings host: localhost # db host port: 3306 # db port user: test_project # db user password: test-project # db user password dbname: test_project # db name memcached: # cache settings host: localhost # memcached host port: 11211 # memcached port
The config.yml
file is strictly parsed, which means that all values defined in this file should meet a configuration definition. SlimApp utilizes symfony/config to support configuration definition and parsing of the configuration file. There is an auto-generated config definition class under src/
directory.
Bootstrap
To begin with using a SlimApp enabled app, let's first have a look at the bootstrap.php
file, under PROJECT_DIR:
<?php use Minhao\TestProject\TestProject; use Minhao\TestProject\TestProjectConfiguration; require_once __DIR__ . "/vendor/autoload.php"; define('PROJECT_DIR', __DIR__); /** @var TestProject $app */ $app = TestProject::app(); $app->init(__DIR__ . "/config", new TestProjectConfiguration(), __DIR__ . "/cache/config"); return $app;
There are a few things to be noticed:
- composer component autoloading is done in this file
- the configuration definition is instantiated on-the-fly, using the auto-generated config definition class
TestProjectConfiguration
- the bootstrap file returns an object of type
TestProject
, which is an extension ofSlimApp
Refer to Config Value
With an instance of TestProject
(hence an instance of SlimApp
), we can access the config value in two slightly different ways:
get value by config key:
<?php use Oasis\SlimApp\SlimApp; use Oasis\Mlib\Utils\DataProviderInterface; /** @var SlimApp $app */ $isDebug = $app->getMandatoryConfig('is_debug', DataProviderInterface::BOOL_TYPE); $logDir = $app->getMandatoryConfig('dir.log', DataProviderInterface::STRING_TYPE); $nonExistingConfig = $app->getMandatoryConfig('non_existing_config'); // will throw $port = $app->getOptionalConfig('db.port', DataProviderInterface::INT_TYPE, 3306); $nonExistingConfig = $app->getOptionalConfig('non_existing_config'); // will return null
NOTE: hierarchical config keys can be concatenated by a dot "."
get value by parameter key:
<?php use Oasis\SlimApp\SlimApp; use Oasis\Mlib\Utils\DataProviderInterface; /** @var SlimApp $app */ $isDebug = $app->getParameter('app.is_debug'); $dbPort = $app->getParameter('app.db.port'); $nonExistingConfig = $app->getParameter('app.non_existing_config'); // will throw
Compared to the first method, the parameter key only prepends the "app." prefix to the config key. And furthermore, all parameter keys are required to exist, or an exception will be thrown when accessed.
In practice, we suggest using the parameter key because it is consistent with how you access parameter in container definitions and twig templates. On the other side, the auto type checking ability when using config key is an useful advantage some times.
Service Container
SlimApp makes use of dependency injection design pattern heavily. Internally, it uses the symfony/dependency-injection component to implement a service container.
There is a centralized service container definition file, in the format of YAML, located under the config
directory. This file is named: services.yml
, and a sample can be found below:
# services.yml imports: - { resource: "external_definition1.yml" } - { resource: "external_definition2.yml" } parameters: default.namespace: - Oasis\Mlib\ - Minhao\TestProject\ services: app: properties: logging: path: '%app.dir.log%' level: debug cli: name: test-project version: '0.1' http: cache_dir: '%app.dir.cache%' routing: path: '%app.dir.config%/routes.yml' namespaces: - Minhao\TestProject\ - Minhao\TestProject\Controllers\ twig: template_dir: '%app.dir.template%' globals: app: '@app' injected_args: - '@entity_manager' memcached: class: Memcached calls: - - addServer - - '%app.memcached.host%' - '%app.memcached.port%' entity_manager: class: Doctrine\ORM\EntityManager factory: - Minhao\TestProject\Database\TestProjectDatabase - getEntityManager
A common services.yml
file consists of three parts:
- imports: this is where you import other service definition files. With the help of imports, we can break large service definition files into smaller and more meaningful pieces.
- parameters: this is an array of parameters in key-value pairs. All parameters defined here, together with those defined in the
config.yml
(accessed by using their coresponding parameter key), can be dereferenced using the "%%" notation, e.g. '%app.memcached.host%' - services: this is the real definition of services. A service is a key referring to an injectable variable. Services can be refered in other services too, using the "@" notation, e.g. '%entity_manager'
Service Description
The way how a service is defined deserves a more detailed explanation. The descriptions (allowed attributes under the service key) can be separated into 3 phases:
- the constructing phase
- the setting phase
- the decoration phase
The Construction Phase
First of all, any service needs to have a type, and this is described using the class
attribute.
NOTE the class name has to be fully qualified unless a prefix namespace can be found in parameter '%default.namespace%'
With the class
defined, we will need to access the object. There are 2 ways to get a service object:
- by instantiating it using constructor
- or by using a factory class (or a factory object) to get the service
services: object.constructed: class: Minhao\TestProject\User arguments: - "John Smith" # name - 25 # age object.factory.provided: class: Minhao\TestProject\User factory: [UserProvider, getUser] # factory class, factory method arguments: - 250008 # student ID
The Setting Phase
After a service object is defined, we can modify the object as well, this is done like the example below:
services: object.modified: class: Minhao\TestProject\User arguments: - "John Smith" # name - 25 # age properties: tel: "1234567890" # accessed as public property age: 40 # accessed as public property, overrides constructor calls: - [setSupervisor, "@another.user"] # method and arguments
As you can see, we can further modify a service either by accessing its properties, or by calling methods on the object.
NOTE same as constructing phase, all attributes defined in setting phase will only be applied once, right after constructing phase
The decoration phase:
There are more powerful techniques to describe a service. Although they are not popularly used in practice, you may still be interested in having a look at them. Follow the guides on Symfony's official website to find out more.
Define the "@app" service
There is one and only one special service that MUST be well described in the services.yml
, and this is the "app" service.
The constructing phase of the "app" service is done automatically and needs little extra attention. It is the properties
attributes that differentiates each application:
services: app: properties: logging: path: %app.dir.log% level: debug handlers: # array extra monolog hanlders - "@log.handler.email" cli: name: Slim App Console version: 1.1 commands: # array of command object - '@cli.command.dummy' http: # http bootstrap config
Logging
SlimApp uses oasis/logging as the logging tool. oasis/logging provides a plug-and-use PSR-3 compatible logging solution, which is built on top of monolog.
By default, SlimApp will install two log handlers:
- The local file log handler, which streams logs to local file system, based on the configured log level
- The local file error handler, which only saves logs to local file system when errors occur (i.e. an error or higher level log is triggered)
And furthermore, for apps run on command line, an additional console log handler is installed. The console log handler will directly write to STDERR with ANSI color decoration enabled.
When defining the "@app" service, the logging
property can be set with the following attributes:
HTTP Kernel
Remember we claimed that SlimApp is a micro framework for both web and console development? It is now time to learn how SlimApp offers us the ability to make web applications in an easy yet powerful fashion.
SlimApp uses oasis/http as its HTTP Kernel implementation. oasis/http is an extension to the widely used Silex framework, and provides a kernel definition strictly implementing the Symfony\Component\HttpKernel\HttpKernelInterface
.
It is already well documented in oasis/http about how to bootstrap an HTTP Kernel. So what we are going to introduce here is much simpler, i.e. how to inject bootstrap configuration in our service definition:
There is a property for the "@app" service, http
, which will be passed to oasis/http as bootstrap configuration. The value of http
must be an array complying with the oasis/http standard. Below is a sample configuration:
services: app: properties: http: routing: path: %app.dir.config%/routes.yml namespaces: - Oasis\SlimApp\Ut\ error_handlers: '@http.error_handler' view_handlers: '@http.view_handler' cors: - path: /cors/* origins: "*"
When everthing is well configured, we can use the HTTP Kernel like how it is demonstrated in the auto-generated front.php
file:
<?php use Minhao\TestProject\TestProject; /** @var TestProject $app */ $app = require_once __DIR__ . "/../bootstrap.php"; $app->getHttpKernel()->run();
Command Line Interface
As discuessed, SlimApp is not only meant for web applications. It also provides a rich featured CLI framework. The underlying implementation heavily uses the symfony/console component. Although it does not require a comprehensive knowledge about symfony/console to start using the basic CLI features of SlimApp, it is recommended to go through the symfony documentation if you would like to extend the framework or make use of some advanced features.
First, let us have a look at the cli
property of the "@app" servie:
ervices: app: properties: cli: name: Slim App Console version: 1.1 commands: # array of command object - '@cli.command.dummy'
There are 3 attributes which can be set:
- name: the name of the console app, which is used when information is asked (like --help)
- version: the version of the console app, used in pair with the name attribute
- commands: an array of app specific commands. The command object must also be a defined service of the type
Symfony\Component\Console\Command\Command
.
The console app can be started from the auto-generated entry script at PROJECT_DIR/bin/.php:
./bin/test-project.php <command>
There is a mandatory command argument, which is used to determine which Command
object to invoke. Everything after the command argument will be interpreted as input arguments and options.
NOTE: it is also possible (and probably more convenient with the help of an IDE with auto-complete feature) to inject supported commands into the CLI by calling the addCommands()
method in console entry script:
<?php use Minhao\TestProject\TestProject; /** @var TestProject $app */ $app = require_once __DIR__ . "/../bootstrap.php"; $console = $app->getConsoleApplication(); $console->addCommands( [ new MyCustomCommandOne(), new MyCustomCommandTwo(), ] ); $console->run();
Writing Your Own Command
To create your own command, you can start by extending the Symfony\Component\Console\Command\Command
class and override at least the configure()
method and the execute()
method:
<?php namespace Minhao\TestProject\Console\Commands; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class MyCustomCommandOne extends Command { protected function configure() { parent::configure(); $this->setName('custom:command:one') ->setDescription("Say hello world!"); } protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln("Hello World!"); } }
NOTE: the command is put into the namespace
Console\Commands\
under root namespace of the project. This is a convention of SlimApp and is advised to be followed.
Using Input Argument
A command without any arguments is useless in most practical scenarios. We can tell a command to accept input arguments.
NOTE: it is worth noticing the difference between command line argument and command input argment. When we execute a command in shell, the shell separates the whole input line into command line arguments delimited by space. When executing our CLI command, the first command line argument ($arg[0]) is the entry script name, and the second ($arg[1]) must be the command name. After that, any additional command line arguments that does not start with a hyphen ('-') is considered to be an input arguemnt (except option values, which follows an input option that requires value).
To enable your command to accept input arguments, you can use the addArgument()
method to declare the arguments you expect, and you can use the getArgument()
method on $input
passed to execut()
to read the argument:
<?php // namespace imports omitted ... class MyCustomCommandOne extends Command { protected function configure() { parent::configure(); $this->setName('custom:command:one') ->setDescription("Say hello world!"); $this->addArgument( 'name', InputArgument::REQUIRED, "give your name here" ); } protected function execute(InputInterface $input, OutputInterface $output) { $name = $input->getArgument('name'); $output->writeln('Hello, ' . $name . '!'); } }
Running the test command will output:
$ ./bin/test-project.php custom:command:one John
Hello, John!
NOTE: you can expect more than one arguments. However, you can only declare OPTIONAL arguments at the end of the list. To be more clear, there cannot be any REQUIRED argument after an OPTIONAL argument.
Using Input Option
Input options are command line arguments that start with one or two hyphen. As the name "opiton" suggests, input options are always optional. An input option name has two different forms: long option name and short option name. Long option name starts with two hyphens ("--") and is mandatory for every option. A short option starts with single hyphen ("-") and is optional.
In addition, there are certain input options which require values attached to them. These option values are set in two ways:
- direct command line argument following the option:
$ ./bin/test-project.php custom:command:one John -m question
- using an equal sign after the long option name:
$ ./bin/test-project.php custom:command:one John --mood=question
To declare and use input options, read the example code below:
<?php // namespace imports omitted ... class MyCustomCommandOne extends Command { protected function configure() { parent::configure(); $this->setName('custom:command:one') ->setDescription("Say hello world!"); $this->addArgument( 'name', InputArgument::REQUIRED, "give your name here" ); $this->addOption( 'mood', 'm', InputOption::VALUE_REQUIRED ); } protected function execute(InputInterface $input, OutputInterface $output) { $name = $input->getArgument('name'); $mood = $input->getOption('mood'); $sign = ($mood == "question") ? "?" : "!"; $output->writeln('Hello, ' . $name . '!'); } }
Executing the command will output like below:
$ ./bin/test-project.php custom:command:one John --mood=question
Hello, John?
The Daemon Sentinel
In practice, a project can have a number of commands to be executed in a pre-determined schedule. Some of the commands need to be executed at a given interval, some need to run more than one instance in parallel, some need to automatically send alert when execution fails, and so on. SlimApp provides a very useful feature called the Daemon Sentinel just to solve this problem.
A Daemon Sentinel itself is also a command. Your application should extend the Oasis\SlimApp\SentinelCommand\AbstractDaemonSentinelCommand
to have your command class:
<?php namespace Minhao\TestProject\Console\Commands; use Oasis\SlimApp\SentinelCommand\AbstractDaemonSentinelCommand; class TestSentinelCommand extends AbstractDaemonSentinelCommand { protected function configure() { parent::configure(); $this->setName('test:daemon'); } }
When executed, the command expects a configuration file as its first argument. The format of the file should be YAML, and a sample is like below:
commands: dummy: # name of the daemon, informative only, use any name meaningful # command name name: dummy:job # command line arguments to pass to the command args: a: %app.name% --tt: true --idx: $PARALLEL_INDEX -vvv: # whether to run in parallel, and if yes, how many parallel: 3 # run only once? if not, command will restart upon previous execution ends once: false # alert on abnormal exit (exit != 0) alert: false # interval: minimum number of seconds between last end and next start interval: 2 # frequency: minimum seconds between two start frequency: 5 # frequency_fixed: default to false, if enabled, commands will start at fixed frequency, no matter if the previous run has finished or not frequency_fixed: false
NOTE: all config values can be used in the format of "%key-to-config-value%" in args setting of command
NOTE: $PARALLEL_INDEX is a special variable in args, denoting the index of the command if executed in parallel. The index starts from 0.