[FEATURE] services and services factory

This commit is contained in:
Marco Stoll 2019-06-20 12:13:45 +02:00
parent a4cb81dee1
commit 78e793f8e3
11 changed files with 755 additions and 4 deletions

View File

@ -1,5 +1,5 @@
{
"name": "fastforward",
"name": "fastforward/fastforward",
"description": "A template for building web applications - part of the Fast Forward Family",
"type": "library",
"license": "MIT",
@ -14,9 +14,7 @@
],
"require": {
"php": ">=7.2",
"fastforward/data-structures": "^1.0.0",
"fastforward/factories": "^1.0.0",
"fastforward/utils": "^1.0.0"
"fastforward/factories": "^1.0.0"
},
"require-dev": {
"phpunit/phpunit": "^8"

173
readme.md Normal file
View File

@ -0,0 +1,173 @@
Fast Forward
========================================================================================================================
by Marco Stoll
- <marco.stoll@rocketmail.com>
- <http://marcostoll.de>
- <https://github.com/marcostoll>
- <https://github.com/marcostoll/FF>
------------------------------------------------------------------------------------------------------------------------
# Introduction - What is Fast Forward?
**Fast Forward** (in short **FF**) is a generic application template for building web and/or command line applications
fast and easy. It addresses the common tasks and provides configurable and/or extendable default implementations for you
to use.
Currently **FF** is composed of the following features:
1. Services and a Service Factory
More features will follow (see the Road Map section below).
# A Warning before you start
**FF** is highly opinionated and depends on a bunch of conventions! So be sure to consult the documentation
before deciding to develop your application based on **FF**.
But if you do **FF** ensures a minimal amount of setup, letting you concentrate your efforts on your business logic
instead of soe framework configuration.
# Dependencies
- FF Family
**FF** makes heavy usage of the **Fast Forward Family** components, a collection of independent components providing
generic implementations (like data structures of design patterns) used by many of **FF**'s features.
# Installation
## via Composer
## manual Installation
# Bootstrapping
## Bootstrap a web application
## Bootstrap a command line application
# Services and the Service Factory
## Services - a definition
From the **FF** perspective a service is a singular component (mostly class) that provide needed functionality as part
of certain domain. Common attributes of services should be
- Make minimal assumptions of the surrounding runtime environment.
- Be stateless.
- Be unit testable.
## Writing services
[**CONVENTION**] Service classes extend `FF\Services\AbstractService`.
[**CONVENTION**] Services should be located in your project's `MyProject\Services` namespace (or any sub namespace
thereof) to be found by the `ServicesFactory`.
***Example: Basic service implementation***
namespace MyProject\Services;
use FF\Services\AbstractService;
class MyService extends AbstractService
{
}
**FF** services can be configured if needed. The `ServicesFactory` will initialize a service's instance with available
config options. But it will be your responsibility to validate that any given options will meet your service's
requirements.
***Example: Configurable service implementation***
namespace MyProject\Services;
use FF\Services\AbstractService;
class MyConfigurableService extends AbstractService
{
/**
* {@inheritDoc}
*/
protected function validateOptions(array $options, array &$errors): bool
{
// place you option validation logic here
// example
if (!isset($options['some-option'])) {
$errors[] = 'missing options [some-options]';
return false;
}
return true;
}
}
## Using the Service Factory
**FF**'s service factory is based on the `AbstractSingletonFactory` of the **FF-Factories** component. Be sure to
consult component's [documentation](https://github.com/marcostoll/FF-Factories) for further information about using
**FF** factories.
To retrieve a service from the factory you have to know thw service's **class identifier**. These class identifiers are
composed of the service's class name prefixed by any sub namespace relative to `FF\Services` (for built-in services) or
`MyProject\Services` (for you custom services).
For convenience reasons there is a shorthand class `SF` that lets you retrieve one or more services with minimal effort.
***Example: Getting a single service***
use FF\Services\SF;
use MyProject\Services\MyService;
/** @var MyService $myService */
$myService = SF::i()->get('MyService'); // finds MyProject\Services\MyService
***Example: Getting multiple services at once***
use FF\Services\Events\EventBroker;
use FF\Services\SF;
use MyProject\Services\MyService;
/** @var EventBroker $eventBroker */
/** @var MyService $myService */
list ($eventBroker, $myService) = SF::i()->get('Events\EventBroker', MyService');
## Extending built-in FF Services
The `ServicesFactory` uses a `FF\Factories\NamespaceClassLocator` locator to find services definitions bases on a class
identifier. To archive this, it searches the list of registered base namespace for any suitable service class **in the
given order**.
This feature lets you sub class and replace **FF**'s built-in service implementations easily by just following the
naming conventions and registering the `ServiceFactory` as shown in the **Bootstrapping** section of this document.
***Example: Extend/Replace a built-in service***
namespace MyProject\Services\Runtime;
use FF\Services\Runtime\ExceptionHandler as FFExceptionHandler;
class ExceptionHandler extends FFExceptionHandler
{
// place your custom logic here
}
###############
// when ever some component refers to the 'Runtime\ExceptionHandler' service via the ServicesFactory
// an instance of your service extension will be used instead of the built-in service
/** @var MyProject\Services\Runtime\ExceptionHandler $exceptionHandler */
$exceptionHandler = SF::get('Runtime\ExceptionHandler');
# Road map
- Events
- Runtime
- Controllers
- Sessions
- Security
- CLI
- ORM
- Bootstrapping

View File

@ -0,0 +1,93 @@
<?php
/**
* Definition of AbstractService
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services;
use FF\Services\Exceptions\ConfigurationException;
/**
* Class AbstractService
*
* @package FF\Services
*/
abstract class AbstractService
{
use ServiceLocatorTrait;
/**
* @var array
*/
protected $options;
/**
* @param array $options
* @throws ConfigurationException
*/
public final function __construct(array $options = [])
{
$this->initialize($options);
}
/**
* @return array
*/
public function getOptions(): array
{
return $this->options;
}
/**
* Retrieves a config option
*
* If no option is present indexed with the given $key, the $default value is returned instead.
*
* @param string $key
* @param null $default
* @return mixed
*/
public function getOption(string $key, $default = null)
{
return $this->options[$key] ?? $default;
}
/**
* Initializes the service
*
* Overwrite this method to place custom service initialization logic.
*
* @param array $options
* @throws ConfigurationException
*/
protected function initialize(array $options)
{
$errors = [];
if (!$this->validateOptions($options, $errors)) {
throw new ConfigurationException($errors);
}
$this->options = $options;
}
/**
* Validates the service's options
*
* Fills $errors with messages regarding erroneous service configuration.
*
* Overwrite this method to do configuration validation for any concrete service depending on specific options.
*
* @param array $options
* @param string[] $errors
* @return bool
*/
protected function validateOptions(array $options, array &$errors): bool
{
return true;
}
}

View File

@ -0,0 +1,27 @@
<?php
/**
* Class ConfigurationException
*
* @package FF\Services\Exceptions
* @author Marco Stoll <m.stoll@core4.de>
* @link http://core4.de CORE4 GmbH & Co. KG
* @filesource
*/
namespace FF\Services\Exceptions;
/**
* Class ConfigurationException
*/
class ConfigurationException extends \RuntimeException
{
/**
* @param string[] $errors
* @param int $code
* @param \Throwable $previous
*/
public function __construct(array $errors, int $code = 0, \Throwable $previous = null)
{
parent::__construct(implode(PHP_EOL, $errors), $code, $previous);
}
}

31
src/Services/SF.php Normal file
View File

@ -0,0 +1,31 @@
<?php
/**
* Definition of SF
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services;
/**
* Class SF
*
* @package FF\Services
*/
class SF extends ServicesFactory
{
/**
* Retrieves the ServicesFactory's singleton instance
*
* @return ServicesFactory
*/
public static function i(): ServicesFactory
{
return parent::getInstance();
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* Definition of ServiceLocatorTrait
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services;
/**
* Trait ServiceLocatorTrait
*
* @package FF\Services
*/
trait ServiceLocatorTrait
{
/**
* Retrieves services from the factory
*
* @param string[] $classIdentifiers
* @return AbstractService|AbstractService[]
*/
protected function getService(string ...$classIdentifiers)
{
return SF::i()->get(...$classIdentifiers);
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* Definition of ServicesFactory
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Services;
use FF\Factories\AbstractSingletonFactory;
use FF\Factories\ClassLocators\ClassLocatorInterface;
use FF\Factories\ClassLocators\NamespaceClassLocator;
use FF\Factories\Exceptions\ClassNotFoundException;
use FF\Services\Exceptions\ConfigurationException;
/**
* Class ServicesFactory
*
* @package FF\Services
*/
class ServicesFactory extends AbstractSingletonFactory
{
/**
* @var ServicesFactory
*/
protected static $instance;
/**
* @var array
*/
protected $servicesOptions;
/**
* Uses a NamespaceClassLocator pre-configured with the FF\Services namespace.
* @param array $servicesOptions
* @see \FF\Factories\ClassLocators\NamespaceClassLocator
*/
public function __construct(array $servicesOptions = [])
{
parent::__construct(new NamespaceClassLocator(__NAMESPACE__));
$this->servicesOptions = $servicesOptions;
}
/**
* Sets the singleton instance of this class
*
* @param ServicesFactory $instance
*/
public static function setInstance(ServicesFactory $instance)
{
self::$instance = $instance;
}
/**
* Retrieves the singleton instance of this class
*
* @return ServicesFactory
* @throws ConfigurationException
*/
public static function getInstance(): ServicesFactory
{
if (is_null(self::$instance)) {
throw new ConfigurationException(['singleton instance of the service factory has not been initialized']);
}
return self::$instance;
}
/**
* Removes the singleton instance of this class
*/
public static function clearInstance()
{
self::$instance = null;
}
/**
* Retrieves one ro more fully initialized services
*
* Returns a single service instance if only one class identifier was given as argument.
* Returns an array of service instances instead if two or more class identifiers were passed. The returned list
* will ordered in the same way as the class identifier arguments have been passed.
*
* @param string ...$classIdentifiers
* @return AbstractService|AbstractService[]
* @throws ClassNotFoundException
* @throws ConfigurationException
*/
public function get(string ...$classIdentifiers)
{
$services = [];
foreach ($classIdentifiers as $classIdentifier)
{
$services[] = parent::create($classIdentifier, $this->getServiceOptions($classIdentifier));
}
return count($services) == 1 ? $services[0] : $services;
}
/**
* {@inheritdoc}
* @return AbstractService
*/
public function create(string $classIdentifier, ...$args)
{
/** @var AbstractService $service */
$service = parent::create($classIdentifier, ...$args);
return $service;
}
/**
* {@inheritDoc}
* @return NamespaceClassLocator
*/
public function getClassLocator(): ClassLocatorInterface
{
return parent::getClassLocator();
}
/**
* Retrieves the options for a specific service
*
* @param string $classIdentifier
* @return array
*/
public function getServiceOptions(string $classIdentifier)
{
return $this->servicesOptions[$classIdentifier] ?? [];
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* Definition of AbstractServiceTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Tests\Services;
use FF\Services\AbstractService;
use FF\Services\Exceptions\ConfigurationException;
use PHPUnit\Framework\TestCase;
/**
* Test AbstractServiceTest
*
* @package FF\Tests
*/
class AbstractServiceTest extends TestCase
{
const TEST_OPTIONS = ['foo' => 'bar'];
/**
* @var MyService
*/
protected $uut;
/**
* {@inheritdoc}
*/
protected function setUp(): void
{
$this->uut = new MyService(self::TEST_OPTIONS);
}
/**
* Tests the namesake method/feature
*/
public function testGetOptions()
{
$this->assertEquals(self::TEST_OPTIONS, $this->uut->getOptions());
}
/**
* Tests the namesake method/feature
*/
public function testGetOption()
{
$this->assertEquals(self::TEST_OPTIONS['foo'], $this->uut->getOption('foo'));
$this->assertNull($this->uut->getOption('unknown'));
}
/**
* Tests the namesake method/feature
*/
public function testGetDefault()
{
$default = 'default';
$this->assertEquals($default, $this->uut->getOption('unknown', $default));
}
/**
* Tests the namesake method/feature
*/
public function testConfigurationException()
{
$this->expectException(ConfigurationException::class);
new MyService(['foo' => 'baz']);
}
}
class MyService extends AbstractService
{
protected function validateOptions(array $options, array &$errors): bool
{
if (isset($options['foo']) && $options['foo'] != 'bar') {
$errors[] = 'foo is not bar';
return false;
}
return parent::validateOptions($options, $errors);
}
}

42
tests/Services/SFTest.php Normal file
View File

@ -0,0 +1,42 @@
<?php
/**
* Definition of SFTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Tests\Services;
use FF\Services\ServicesFactory;
use FF\Services\SF;
use PHPUnit\Framework\TestCase;
/**
* Test SFTest
*
* @package FF\Tests
*/
class SFTest extends TestCase
{
/**
* {@inheritdoc}
*/
public static function setUpBeforeClass(): void
{
$servicesFactory = new ServicesFactory();
SF::setInstance($servicesFactory);
}
/**
* Tests the namesake method/feature
*/
public function testI()
{
$sf = SF::i();
$this->assertInstanceOf(ServicesFactory::class, $sf);
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* Definition of ServicesFactoryTest
*
* @author Marco Stoll <marco@fast-forward-encoding.de>
* @copyright 2019-forever Marco Stoll
* @filesource
*/
declare(strict_types=1);
namespace FF\Tests\Services;
use FF\Factories\Exceptions\ClassNotFoundException;
use FF\Services\AbstractService;
use FF\Services\Exceptions\ConfigurationException;
use FF\Services\ServicesFactory;
use PHPUnit\Framework\TestCase;
/**
* Test ServicesFactoryTest
*
* @package FF\Tests
*/
class ServicesFactoryTest extends TestCase
{
const TEST_OPTIONS = ['ServiceOne' => ['foo' => 'bar']];
/**
* {@inheritdoc}
*/
public static function setUpBeforeClass(): void
{
ServicesFactory::clearInstance();
}
/**
* Tests the namesake method/feature
*/
public function testGetInstanceConfigException()
{
$this->expectException(ConfigurationException::class);
ServicesFactory::getInstance();
}
/**
* Tests the namesake method/feature
*/
public function testSetGetInstance()
{
$instance = new ServicesFactory(self::TEST_OPTIONS);
$instance->getClassLocator()->prependNamespaces(__NAMESPACE__);
ServicesFactory::setInstance($instance);
$this->assertSame($instance, ServicesFactory::getInstance());
}
/**
* Tests the namesake method/feature
*
* @depends testSetGetInstance
*/
public function testSetServiceOptions()
{
$this->assertEquals(
self::TEST_OPTIONS['ServiceOne'],
ServicesFactory::getInstance()->getServiceOptions('ServiceOne')
);
$this->assertEquals([], ServicesFactory::getInstance()->getServiceOptions('unknown'));
}
/**
* Tests the namesake method/feature
*
* @depends testSetGetInstance
*/
public function testGetSingle()
{
$service = ServicesFactory::getInstance()->get('ServiceOne');
$this->assertInstanceOf(ServiceOne::class, $service);
$this->assertEquals(self::TEST_OPTIONS['ServiceOne'], $service->getOptions());
}
/**
* Tests the namesake method/feature
*
* @depends testSetGetInstance
*/
public function testGetMultiples()
{
$services = ServicesFactory::getInstance()->get('ServiceOne', 'ServiceOne');
$this->assertEquals(2, count($services));
$this->assertInstanceOf(ServiceOne::class, $services[0]);
$this->assertInstanceOf(ServiceOne::class, $services[1]);
}
/**
* Tests the namesake method/feature
*
* @depends testSetGetInstance
*/
public function testGetClassNotFound()
{
$this->expectException(ClassNotFoundException::class);
ServicesFactory::getInstance()->get('ServiceUnknown');
}
}
class ServiceOne extends AbstractService
{
protected function validateOptions(array $options, array &$errors): bool
{
if (isset($options['foo']) && $options['foo'] != 'bar') {
$errors[] = 'foo is not bar';
return false;
}
return parent::validateOptions($options, $errors);
}
}
class ServiceTwo extends AbstractService
{
}

8
tests/testsuite.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
<testsuites>
<testsuite name="FF">
<directory>./</directory>
</testsuite>
</testsuites>
</phpunit>