Bienvenue sur la documentation du framework et des composants Objective PHP¶
maintoc¶
Sommaire
Introduction¶
Quoi ?¶
Objective PHP est un framework écrit en PHP7 et utilisant de nombreux objets. D’où son nom - il n’y pas d’autres origine à ce nom, à part un clin d’oeil à Objective C bien sûr :)
Est-ce qu’Objective PHP est un framework fullstack ? S’agit-il d’un micro-framework ? En fait, ni l’un ni l’autre. Nous avons pris l’habitude de le qualifier de mini-framework, pour souligner le fait qu’il se situe quelque part entre les deux premiers : il fournit nettement plus qu’un micro-framework, sans aller aussi un loin qu’un fullstack.
Objective PHP a pour rôle de gérer les workflows applicatifs, et de laisser les développeurs faire leur travail. Ni plus, ni moins.
Concernant les composants de haut niveau, comme les générateurs de formulaires ou les ORM par exemple, nous pensons qu’il est plus efficace de laisser les développeurs embarqués leurs outils habituels dans Objective PHP plutôt que de les forcer à utiliser nos propres implémentations.
Pourquoi ?¶
Vous vous demandez peut-être : pourquoi est-ce que ces gens se compliquent la vie à créer encore un autre framework PHP ? La réponse est assez simple : nous ne voulons plus passer plus de temps à comprendre et maîtriser le framework qu’à implémenter les véritables fonctionnalités des applications.
la productivité est le but principal d’Objective PHP
En outre, nous avons pensé que travailler sur un nouveau framework serait l’occasion d’envisager la question des performances sous un angle différent. La plupart des frameworks utilisent du cache pour atteindre des performances acceptables. Slit, le cache peut aider. Un peu. Mais une que vous avez mis en cache vos composants peu performants, que pouvez-vous faire de plus ? Rien.
les performances sont la deuxième préoccupation majeure d’Objeective PHP
Comment ?¶
L’idée clé pour aider les développeurs à être à l’aise et efficaces avec Objective PHP est de réduire au minimum le nombre de mécanismes différents nécessaires pour réaliser les tâches que le framework doit prendre en charge.
Après de nombreux essais, nous en sommes arrivés à la combinaison des deux concepts centraux suivants :
des Middlewares pour définir le Workflow
des événements pour faciliter la communication entre les composants de l’application
Démarrer avec Objective PHP¶
Pré-requis¶
Le pré-requis principal nécessaire pour utiliser Objective PHP est PHP7
Si vous ne l’avez pas encore installé, consultez le site officiel de PHP où vous trouverez les instructions nécessaires à l’installation de PHP7 sur votre machine de dévelopmment.
Installation¶
La façon la plus simple de démarrer un projet avec Objective PHP est d’utiliser la fonctionnalité “create-project” de composer et d’obtenir le starter kit d’Objective PHP :
The following command assumes composer is available in your current PATH:
composer create-project -s dev objective-php/starter-kit op-quick-start
op-quick-start devrait être remplacé par n’importe quel identifiant correspondant à votre projet réel.
Démarrer un serveur¶
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
Et voilà ! Si tout s’est bien passé, vous devriez pouvoir consulter la page d’accueil du starter kit en vous rendant sur http://localhost:8080.
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) andrun()
(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']
}
}