Welcome to Objective PHP Framework and Components documentation

Contents:

Introduction

What?

Objective PHP is a lightweight framework written in PHP7 with OOP in mind. This is why it’s called that - there is other reason, except a pun on Objective C of course :)

Is Objective PHP a fullstack framework? Is it a micro-framework? Actually, neither. We use to call it a mini-framework, meaning that it’s somewhere in between: it provides the developers with much more than a micro-framework does, but also less than a fullstack.

Objective PHP aims at handling applicative workflows, then let the developer do their work. No more, no less.

For higher level components, like Forms generators or ORMs for instance, we thought that it would be more efficient to let developers bring their usual tools in Objective rather than forcing them to use our own alternatives.

Why?

You may ask yourself: why did those guys bothered with another PHP framework? The answer is quite simple: we’re bored with spending more times understanding and masterizing the framework itself than with working on the actual applications features.

productivity is the main target of Objective PHP

On top of that, we thought that working on a new framework would also be an opportunity to consider performances in a different way. Most frameworks rely on cache to offer decent performances. Well, cache can help. A bit. But once you cached the poor performing components, what more can you do? Nothing.

performance is the second major concern of Objective PHP

How?

The key idea to help developers getting efficient and comfortable with Objective PHP is to reduce as much as possible the number of different mechanism to achieve essential tasks the framework has to handle.

After many tries, we came down to a combination of two central concepts:

  • Middlewares to set up a workflow
  • Events to ease the connexion between the application components

Getting started with Objective PHP

Pre-requisites

The most important pre-requisite needed to use Objective PHP is PHP7.

If you don’t have it installed yet, please take a look at Official PHP website and read instruction about PHP7 installation on your development machine.

Installation

The easiest way to start a project with Objective PHP is to use composer’s “create-project” feature and get the Objective PHP Starter Kit:

The following command assumes composer is available in your current PATH:

composer create-project -s dev objective-php/starter-kit op-quick-start

Where op-quick-start should be replaced with anything matching your actual project name.

Starting a server

To quickly run and access the starter application, you can use the internal PHP HTTP server. The easiest way for that being launching the provided serve.sh script:

cd op-quick-start
./server.sh [-p PORT=8080]

Or you can launch it manually:

cd op-quick-start
php -S localhost:8080 -t public

That’s it! If everything went fine, you should be able to point a browser to http://localhost:8080 and access the starter kit home page.

Repositories

While the objective-php/starter-kit repository is the easiest way to get started with Objective PHP, all repositories can be accessed and fetched on the project organization page.

Setting up the application

Autoloading

First of all, application classes have to be autoloaded. Objective PHP exclusively rely on composer autoloader. So, to make your classes autoloadable, just add the appropriate directive in the composer.json file:

"autoload": {
    "psr-4": {
        "App\\": "app/src"
    }
}

Where App should match your application main namespace. Note that the Objective PHP composer.json file already contains such a directive, using Project as namespace. You’re invited to update this setting to match your own standards.

Note

Any change to the autoload section of composer.json needs the command composer dumpautoload to be executed to make changes available to the project.

The Application class

The first class you have to create is the Application class. It should extend ObjectivePHP\Application\AbstractApplication, and be placed in your main namespace.

Application instance will the most important object in your application, since it will allow to link all components by being passed as main argument to all middlewares and more generally all invokables (keep on reading to gert more information about those concepts).

Barely all logic in the Application class lays in the Application::init() method, which is automatically triggered when Application::run() is called.

Defining a workflow

It is very important to understand that Objective PHP, as a framework, doesn’t provide the application with an hardcoded workflow, but with every components and mechanisms that are needed to create a workflow.

Note

While Objective PHP, as a framework, doesn’t provide a workflow, the Objective PHP Starter Kit does!

Declaring Steps

The workflow in Objective PHP is defined by first declaring Steps, then plugging Middlewares to each step. Since several middlewares can be plugged into each step, middlewares are stacked, and will be run one after the other, in the same order they were plugged.

Declaring steps is quite trivial (remember this should take place in Application::init()):

$this->addSteps('first-step', 'second-step' /* , 'step-n' */ );

Yes, a step is nothing more, from the developer point view, than a label. When running the application, steps also are run in the same order they were declared using addSteps().

Plugging Middlewares

Middlewares are basically small chunks of code designed to handle very specific parts of the workflow. Objective PHP extends this definition to Packages (Objective PHP extensions), that are considered as parts of the application.

Note

Most common Middelwares (for handling routing, action excution, rendering, etc.) are provided with the objective-php/application package and are plugged by default in the starter kit Project\Application::init() method.

Basic usage

Middlewares are not plugged directly into the application, but into Steps (see previous section). This allow a simple way to sequence middlewares execution order.

$this->getStep('bootstrap')->plug(
    function(ApplicationInterface $app) {
        /* anonymous function is an invokable,
           and as such can be used as a Middleware */
    }
);
Aliasing

When a middleware is plugged into a Step, it is possible to alias it using the Step::as() method. Aliasing a middleware is useful for handling substitution: when plugging a middleware with a given alias, if another one was previously plugged with the same alias, the former will take the latter place in the stack.

// this plugs the invokable class AnyMiddleware as 'initializer'
$this->getStep('bootstrap')->plug(AnyMiddleware::class)->as('intializer');

// this plugs the another invokable class OtherMiddleware also as 'initializer'
$this->getStep('bootstrap')->plug(OtherMiddleware::class)->as('intializer');

// at execution time, only OtherMiddleware will actually be run

Objective PHP also offers to use aliasing to plug default middlewares only. By aliasing a middleware using Step::asDefault, this middleware will be actually registered only if no other was already plugged using the same alias.

This is used for instance in starter kit to plug default operations middlewares, as router: if a package plugs a router middleware, the default one will simply be ignored:

// register custom router
if($whatever = true)
{
    $this->getStep('route')->plug(CustomerRouter::class)->as('router');
}


// this one will be ignored because CustomerRouter was aliased as router prior to SimpleRouter
$this->getStep('route')->plug(SimpleRouter::class)->asDefault('router');

Note

Later on, aliases will also permit to fetch middleware returned values.

Execution filters

Objective PHP allow the developer to filter middlewares actual execution by providing the Step::plug() method with extra invokables, expected to return a boolean. In this case, the middleware will be run only if all filter invokables return true.

$this->getStep('first-step')->plug(
        Middleware::class,
        function(ApplicationInterface $app) {
            return $app->getEnv() == 'development');
        }
);

This very simple mechanism allow the developer to setup a very flexible and dynamic workflow, with little efforts. For instance, it is possible to activate or not a middleware based on current date, user profile, environment variable, and so on. Since the filters are exepected to return a boolean, they can implement a decision mechanism based on virtually anything.

Objective PHP provides by default several filters, like UrlFilter or ContentTypeFilter, located in Application\Workflow\Filter. Those default filter ease most typical filtering needs:

// AnyMiddleware will be run only if URL matches the "/admin/*" pattern
$this->getStep('action')->plug(AnyMiddleware::class, new UrlFilter('/admin/*'));

Configuration

Concept

In many frameworks, developer has to deal with usually huge structured data to represent the application and application components configuration. This approach has several major drawbacks: it leads to very hard to read and maintain files, and mostly, it needs the developer to know by heart all configuration directives and options.

For Objective PHP, we tried hard to find another way to handle configuration directives, a way that would address those problems. After several experiments, we came up with an objective approach: all configuration directives are exposed as objects, and configuration files only contain one-level arrays filled with instances of ObjectivePHP\Config\DirectiveInterface.

Objective PHP exposes this interface, but also several abstract classes to ease Directives conception. There a three kind of directives at this time: SingleValueDirective, StackedValueDirective and SingleValueDirectiveGroup. Their behavior is described later in this chapter.

All directives of a project configuration are imported in an ObjectivePHP\Confi\Config instance, available by default through Application::getConfig(), making it available almost everywhere in the application.

Config object

The Config object extends ObjectivePHP\Primitives\Collection\Collection, and as such offers lots of high-level manipulation methods on the configuration data. This object can import DirectiveInterface instances through either Config::import() (one by one) or Config::fromArray() (an array of directives at once).

Once imported, the DirectiveInterface object is not kept as is. It is used by the Config object to create or merge entries in the configuration data. The configuration directive value can then be fetched using the Config::get($key, $default = null) method, as one would do on any Collection.

Note

Directive key (or prefix when working with group) are equals to the classes name, so that it is not needed to remember keys, and prevent from making typos when referring a given directive in the Config object.

Directive types

Single Value

A SingleValueDirective can contain only one value, which can be scalar or structured (object, array). In case the same Directive is imported twice or more, the latest imported overwrites the previous one by default:

class Single extends ObjectivePHP\Config\SingleValueDirective
{
    // default mechanism inherited from abstract is enough
}

$config->import(new Single('x'))
       ->import(new Single('y'));
$config->get(Single::class) == "y";

This behaviour can be altered by changing the DirectiveMerge policy:

$config->import(new Single('x'))
       ->setMergePolicy(MergePolicy::COMBINE)
       ->import(new Single('y'));
$config->get(Single::class) == ["x", "y"];
Stacked Value

A StackedValueDirective is pretty close to the SingleValue used with the COMBINE merge policy, but with a major difference: its value is always an array, even if only one directive of that kind is imported into the Config object:

class Stacked extends ObjectivePHP\Config\StackedValueDirective
{
    // default mechanism inherited from abstract is enough
}

$config->import(new Stacked('x'));
$config->get(Stacked::class) == ["x"];

$config->import(new Stacked('y'));
$config->get(Stacked::class) == ["x", "y"];
Single Value Group

With this directive type, values will be handled just like with Single Value, especially the merge policy, but with a main difference: each single entry in a Single Value Group has an individual identifier, that will be concatenated to the group prefix:

class Grouped extends ObjectivePHP\Config\SingleValueDirectiveGroup
{

}

$config->import(new Grouped('first', 'first value');
$config->import(new Grouped('second', 'second value');

$config->get(Grouped::class . '.first') == 'first value';

// all grouped directives can be fetched as new Config object using subset()
$config->subset(Grouped::class)->toArray() == ['first' => 'first value', 'second' => 'second value'];

Note

While fetching syntax might not be as intuitive as one could expect, remember that the idea behind all this is that application developers should only deal with directives instantiation, since configuration directives are exepexted to be used by the framework itself and components. All other, arbitrary, application (especially business) parameters should be handled using Application::setParam() and Application::getParam(), not Config.

Default directives

Objective PHP and its packages comes with a few directives:

ObjectivePHP\Application\Config
Class Type Description
ActionNamespace Stack Namespace prefixes where to search for action classes
ApplicationName Single Application name
LayoutsLocations Stack Paths where to search for layout scripts
Route Group Simple Router route definitions
ViewsLocations Stack Paths where to search for view scripts (optional)
ObjectivePHP\ServicesFactory\Config
Class Type Description
Service Group Service specification
ObjectivePHP\EloquentPackage\Config
Class Type Description
EloquentCapsule Group Eloquent ORM Capsule DB connection configuration
ObjectivePHP\DoctrinePackage\Config
Class Type Description
EntityManager Group Doctrine Entity manager and DB connection

CLI commands

Concept

For whatever reason, one could need to run CLI scripts to manipulate some date in their application. Either for administration purpose or to create workers for instance. In any case, it’s often very useful to access the business objects of the application in the command line commands.

ObjectivePHP provides the developer with a very simple but powerful support for such commands. The idea was to reuse 100% of the original bootstrap file (typically public/index.php, which is by the way the default path where the bootstrap is looked after).

Quick start

Create a cli action
To implement a CLI command using objective-php/cli, you essentially have to extends one abstract action class:
and implement both __construct() (for setting up the command) and run() (to actually run the command) methods on it:
namespace My\Project\CliActions;

use ObjectivePHP\Cli\Action\AbstractCliAction;

class HelloWorld extends AbstractCliAction
{
    /**
     * HelloWorld constructor.
     */
    public function __construct()
    {
        // this is the route to the command
        $this->setCommand('hello');
        // this is the description - automatically
        $this->setDescription('Sample command that kindly greets the user');
    }

    /**
     * @param ApplicationInterface $app
     */
    public function run(ApplicationInterface $app)
    {
        $c = new CLImate();
        $c->out('Hello world!);
    }
}

Note

CLImate, from The league of extraordinary packages, is bundled by default with objective-php/cli since it is used internally to produce the usage command output, but is absolutely not mandatory. That said, you should definitely consider it to handle your output and formatting :)

Setup cli commands routing

Once your command is ready to run, you have to setup the application to make it able to route cli requests. This is done by registering an instance of ObjectivePHP\Cli\Router\CliRouter in the MetaRouter middleware (assuming you’re using it of course).

This has to be done in the init() method of your Application class:

$cliRouter = new CliRouter();
$router->register($cliRouter);

Then, on the same instance of the router, register your newly created cli command:

$cliRouter->registerCommand(HelloWorld::class);

That’s it! You can now run your command by executing vendor/bin/op hello from the root of your project.

Listing available commands

If you’re in doubt or are just discovering a new project likely to provide you with CLI commands, you may not know what commands are available. To list those available commands, just run th op script without any argument:

starter-kit$ vendor/bin/op
Objective PHP Command Line Interface wrapper
No command has been specified. List of available commands:

     - usage                 List available commands and parameters
     - hello                 Sample command that kindly greets the user

Note

The usage command will always be listed since it’s automatically added to the set of available commands by objective-php/cli itself. This is actually the command that produces this very output.

Parameters

It’s not unusual that a CLI script requires some parameters, switches and/or arguments. Of course, objective-php/cli natively supports such a mechanism. There are currently three kinds of parameters: Toggles, Params and Arguments.

All of them implement the ObjectivePHP\Cli\Action\Parameter\ParameterInterface interface class, which states that a CLI parameter class should expose the following methods:

public function getDescription() : string;

public function getShortName() : string;

public function getLongName() : string;

public function hydrate(array $argv) : array;

public function getValue();

public function getOptions() : int;

This API is mostly self-explaining: a parameter should always provide a description, a short and/or a long name, a value and options flag value. On top tf that, any parameter should be able to pick its value from the argv stack.

Independently from the actual type of parameter you defined for your CLI action, they all are accessible through the getParam() shortcut method. This method expects a $param``name as first parameter, then an optional ``$default value and finally, an optional $origin, which can be cli (default) or env, to access environment variables.

All parameters are set on a command using expects(ParameterInterface $parameter, string $description) on the AbstractCliAction class. Usage examples will be provided with details for each kind of parameter.

At the time being, there are three ParameterInterface implementations provided by objective-php/cli:

  • Toggle
  • Param
  • Argument

Detailed usage of these classes is presented after the common naming and options paragraphs.

Common features
Naming

The ParameterInterface class states taht a parameter should/could have both a short and a long name. Actually, this will depend on the kind of parameter you’re setting up. At the time of writing, Param and Toggle classes both support defining a short and/or a long name, while Argument only expects a long name.

For parameters accepting both names, the setName($name) will behave as follow:

  • if strlen($name) === 1, $name is considered as a short name
  • if strlen($name) > 1, $name is considered as a long name
  • if is_array($name), $name is supposed to contains [‘shortName’ => ‘longName’]

Note

the setName() method will also be triggered when passing $name to the __construct() method.

Note

in case you pass an array, if ‘shortName’ length is greater than 1, an Exception will be thrown.

Options

All parameters can receive option flags. There are two reserved options defined in ParameterInterface:

  • MANDATORY if applied, the command will exit after displaying the usage when the parameter is not provided on the command line.

  • MULTIPLE this option’s behavior depends on the parameter it’s applied to:
    • Toggle are multiple by default, meaning that all occurrences of the parameter on the command line will increment the toggle value by 1.
    • Param when multiple option is set, multiple occurrences of the parameter on the command line are allowed and the value of the parameter always is an array.
Available parameters
Toggle

Toggles are kind of switches: they don’t expect any value on the command line. Their value will be the equal to the number of times they are passed on the command line. Short and long name occurrences are aggregated.

class Command extends AbstractCliAction
{
    public function __construct() {
        $this->setCommand('trigger');
        $this->expects(new Toggle(['v' => 'verbose'], 'Verbose output'));
    }

    public function run(ApplicationInterface $app)
    {
        $v = $this->getParam('verbose');

        // with 'op trigger --verbose' ........... $v === 1
        // with 'op trigger --verbose --verbose' . $v === 2
        // with 'op trigger -v --verbose' ........ $v === 2
        // with 'op trigger -vv' ................. $v === 2
        // with 'op trigger -vv --verbose' ....... $v === 3

    }
}
Param

Params expect a value to be associated to the parameter. Value can be separated from the parameter using a space or an equal sign. When both a short and a long name are set, the latter has priority on the former.

class Command extends AbstractCliAction
{
    public function __construct() {
        $this->setCommand('trigger');
        $this->expects(new Param(['o' => 'output'], 'Output directory'));
    }

    public function run(ApplicationInterface $app)
    {
        $o = $this->getParam('output');

        // with 'op trigger --output some/dir' .................. $o === 'some/dir'
        // with 'op trigger --output=some/dir' .................. $o === 'some/dir'
        // with 'op trigger -o=some/dir' ........................ $o === 'some/dir'
        // with 'op trigger --output=other/dir -o some/dir ' .... $o === 'other/dir'

    }
}

This default behavior, considering long name parameters have precedence over short ones, can be altered by applying the MULTIPLE option to a parameter. In this case, the parameter value will always be an array,

class Command extends AbstractCliAction
{
    public function __construct() {
        $this->setCommand('trigger');
        $this->expects(new Param(['o' => 'output'], 'Output directory', Param::MULTIPLE));
    }

    public function run(ApplicationInterface $app)
    {
        $o = $this->getParam('output');

        // with 'op trigger --output=some/dir' ............... $o === ['some/dir']
        // with 'op trigger --output=other/dir -o some/dir ' . $o === ['other/dir', 'some/dir']

    }
}
Argument

Arguments are a bit special compared to the other two: they are considered as positional parameters. As such, they will always be treated after all other type of parameters. Also, the order they are passerd to expects() matters. The first Argument parameter to be registered will match the first argument from the CLI that is not a Toggle or a Param.

class Command extends AbstractCliAction
{
    public function __construct() {
        $this->setCommand('trigger');
        $this->expects(new Argument(['o' => 'output'], 'Output directory', Param::MULTIPLE));
    }

    public function run(ApplicationInterface $app)
    {
        $o = $this->getParam('output');

        // with 'op trigger other/dir some/dir ' ..... $o === ['other/dir', 'some/dir']

    }
}

When an Argument is marked as MANDATORY, it becomes forbidden to stack extra optional (i.e. not flagged as mandatory) arguments, and when marked as MULTIPLE, no other argument can be expected, since all positional arguments after it will be aggregated to its value.

class Command extends AbstractCliAction
{
    public function __construct() {
        $this->setCommand('trigger');
        $this->expects(new Argument(['i' => 'output'], 'Input directory', Param::MANDATORY));
        $this->expects(new Argument(['o' => 'output'], 'Output directory'));
    }

    public function run(ApplicationInterface $app)
    {
        $i = $this->getParam('input');
        $o = $this->getParam('output');

        // with 'op trigger other/dir some/dir ' ..... $i === 'other/dir' and $o === 'some/dir']

    }
}