From d49e49c364c1b73e4e4e3b52dc10ee9d0150e447 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Fri, 22 Sep 2017 14:02:02 +0200 Subject: [PATCH] Implemented service provider functionality --- config/app.php | 9 ++ includes/engelsystem_provider.php | 13 +- .../helper/internationalization_helper.php | 2 +- src/Application.php | 80 +++++++++- src/Container/ServiceProvider.php | 31 ++++ src/helpers.php | 50 ++++-- tests/Unit/ApplicationTest.php | 147 +++++++++++++++++- 7 files changed, 310 insertions(+), 22 deletions(-) create mode 100644 config/app.php create mode 100644 src/Container/ServiceProvider.php diff --git a/config/app.php b/config/app.php new file mode 100644 index 00000000..fe0a97c1 --- /dev/null +++ b/config/app.php @@ -0,0 +1,9 @@ + [ + ], +]; diff --git a/includes/engelsystem_provider.php b/includes/engelsystem_provider.php index e1669c57..3067ab62 100644 --- a/includes/engelsystem_provider.php +++ b/includes/engelsystem_provider.php @@ -26,6 +26,13 @@ require_once __DIR__ . '/autoload.php'; $app = new Application(realpath(__DIR__ . DIRECTORY_SEPARATOR . '..')); +/** + * Bootstrap application + */ +$appConfig = $app->make(Config::class); +$appConfig->set(app('path.config') . '/app.php'); +$app->bootstrap($appConfig); + /** * Load configuration */ @@ -40,6 +47,10 @@ if (file_exists(__DIR__ . '/../config/config.php')) { )); } + +/** + * Configure application + */ date_default_timezone_set($config->get('timezone')); @@ -55,7 +66,7 @@ $app->instance('request', $request); /** * Check for maintenance */ -if ($config->get('maintenance')) { +if ($app->get('config')->get('maintenance')) { echo file_get_contents(__DIR__ . '/../templates/maintenance.html'); die(); } diff --git a/includes/helper/internationalization_helper.php b/includes/helper/internationalization_helper.php index efbe5db5..7fa6518b 100644 --- a/includes/helper/internationalization_helper.php +++ b/includes/helper/internationalization_helper.php @@ -36,7 +36,7 @@ function gettext_init() } gettext_locale(); - bindtextdomain('default', realpath(__DIR__ . '/../../locale')); + bindtextdomain('default', app('path.lang')); bind_textdomain_codeset('default', 'UTF-8'); textdomain('default'); } diff --git a/src/Application.php b/src/Application.php index 80538396..b62b28a9 100644 --- a/src/Application.php +++ b/src/Application.php @@ -2,7 +2,9 @@ namespace Engelsystem; +use Engelsystem\Config\Config; use Engelsystem\Container\Container; +use Engelsystem\Container\ServiceProvider; use Psr\Container\ContainerInterface; class Application extends Container @@ -10,6 +12,16 @@ class Application extends Container /** @var string|null */ protected $appPath = null; + /** @var bool */ + protected $isBootstrapped = false; + + /** + * Registered service providers + * + * @var array + */ + protected $serviceProviders = []; + /** * Application constructor. * @@ -36,15 +48,73 @@ class Application extends Container } /** + * @param string|ServiceProvider $provider + * @return ServiceProvider + */ + public function register($provider) + { + if (is_string($provider)) { + $provider = $this->get($provider); + } + + $this->serviceProviders[] = $provider; + + $provider->register(); + + if ($this->isBootstrapped) { + $this->call([$provider, 'boot']); + } + + return $provider; + } + + /** + * Boot service providers + * + * @param Config|null $config + */ + public function bootstrap(Config $config = null) + { + if ($this->isBootstrapped) { + return; + } + + if ($config instanceof Config) { + foreach ($config->get('providers', []) as $provider) { + $this->register($provider); + } + } + + foreach ($this->serviceProviders as $provider) { + $this->call([$provider, 'boot']); + } + + $this->isBootstrapped = true; + } + + protected function registerPaths() + { + $appPath = $this->appPath; + + $this->instance('path', $appPath); + $this->instance('path.config', $appPath . DIRECTORY_SEPARATOR . 'config'); + $this->instance('path.lang', $appPath . DIRECTORY_SEPARATOR . 'locale'); + } + + /** + * Set app base path + * * @param string $appPath * @return static */ public function setAppPath($appPath) { + $appPath = realpath($appPath); $appPath = rtrim($appPath, DIRECTORY_SEPARATOR); $this->appPath = $appPath; - $this->instance('path', $appPath); + + $this->registerPaths(); return $this; } @@ -56,4 +126,12 @@ class Application extends Container { return $this->appPath; } + + /** + * @return bool + */ + public function isBooted() + { + return $this->isBootstrapped; + } } diff --git a/src/Container/ServiceProvider.php b/src/Container/ServiceProvider.php new file mode 100644 index 00000000..2a1bbebf --- /dev/null +++ b/src/Container/ServiceProvider.php @@ -0,0 +1,31 @@ +app = $app; + } + + /** + * Register container bindings + */ + public function register() { } + + /** + * Called after other services had been registered + */ + public function boot() { } +} diff --git a/src/helpers.php b/src/helpers.php index de303963..c3c727ec 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -23,6 +23,15 @@ function app($id = null) return Application::getInstance()->get($id); } +/** + * @param string $path + * @return string + */ +function base_path($path = '') +{ + return app('path') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path); +} + /** * Get or set config values * @@ -46,6 +55,15 @@ function config($key = null, $default = null) return $config->get($key, $default); } +/** + * @param string $path + * @return string + */ +function config_path($path = '') +{ + return app('path.config') . (empty($path) ? '' : DIRECTORY_SEPARATOR . $path); +} + /** * @param string $key * @param mixed $default @@ -78,22 +96,6 @@ function session($key = null, $default = null) return $session->get($key, $default); } -/** - * @param string $template - * @param mixed[] $data - * @return Renderer|string - */ -function view($template = null, $data = null) -{ - $renderer = app('renderer'); - - if (is_null($template)) { - return $renderer; - } - - return $renderer->render($template, $data); -} - /** * @param string $path * @param array $parameters @@ -109,3 +111,19 @@ function url($path = null, $parameters = []) return $urlGenerator->to($path, $parameters); } + +/** + * @param string $template + * @param mixed[] $data + * @return Renderer|string + */ +function view($template = null, $data = null) +{ + $renderer = app('renderer'); + + if (is_null($template)) { + return $renderer; + } + + return $renderer->render($template, $data); +} diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index 53fe3109..78310134 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -3,19 +3,23 @@ namespace Engelsystem\Test\Config; use Engelsystem\Application; +use Engelsystem\Config\Config; use Engelsystem\Container\Container; +use Engelsystem\Container\ServiceProvider; use PHPUnit\Framework\TestCase; +use PHPUnit_Framework_MockObject_MockObject; use Psr\Container\ContainerInterface; +use ReflectionClass; class ApplicationTest extends TestCase { /** - * @covers \Engelsystem\Application::__construct - * @covers \Engelsystem\Application::registerBaseBindings + * @covers \Engelsystem\Application::__construct + * @covers \Engelsystem\Application::registerBaseBindings */ public function testConstructor() { - $app = new Application(); + $app = new Application('.'); $this->assertInstanceOf(Container::class, $app); $this->assertInstanceOf(ContainerInterface::class, $app); @@ -27,4 +31,141 @@ class ApplicationTest extends TestCase $this->assertSame($app, Application::getInstance()); $this->assertSame($app, Container::getInstance()); } + + /** + * @covers \Engelsystem\Application::setAppPath + * @covers \Engelsystem\Application::registerPaths + * @covers \Engelsystem\Application::path + */ + public function testAppPath() + { + $app = new Application(); + + $this->assertFalse($app->has('path')); + + $app->setAppPath('.'); + $this->assertTrue($app->has('path')); + $this->assertTrue($app->has('path.config')); + $this->assertTrue($app->has('path.lang')); + + $this->assertEquals(realpath('.'), $app->path()); + $this->assertEquals(realpath('.') . '/config', $app->get('path.config')); + + $app->setAppPath('./../'); + $this->assertEquals(realpath('../') . '/config', $app->get('path.config')); + } + + /** + * @covers \Engelsystem\Application::register + */ + public function testRegister() + { + $app = new Application(); + + $serviceProvider = $this->mockServiceProvider($app, ['register']); + $serviceProvider->expects($this->once()) + ->method('register'); + + $app->register($serviceProvider); + + $anotherServiceProvider = $this->mockServiceProvider($app, ['register', 'boot']); + $anotherServiceProvider->expects($this->once()) + ->method('register'); + $anotherServiceProvider->expects($this->once()) + ->method('boot'); + + $app->bootstrap(); + $app->register($anotherServiceProvider); + } + + /** + * @covers \Engelsystem\Application::register + */ + public function testRegisterBoot() + { + $app = new Application(); + $app->bootstrap(); + + $serviceProvider = $this->mockServiceProvider($app, ['register', 'boot']); + $serviceProvider->expects($this->once()) + ->method('register'); + $serviceProvider->expects($this->once()) + ->method('boot'); + + $app->register($serviceProvider); + } + + /** + * @covers \Engelsystem\Application::register + */ + public function testRegisterClassName() + { + $app = new Application(); + + $mockClassName = $this->getMockClass(ServiceProvider::class); + $serviceProvider = $this->getMockBuilder($mockClassName) + ->setConstructorArgs([$app]) + ->setMethods(['register']) + ->getMock(); + + $serviceProvider->expects($this->once()) + ->method('register'); + + $app->instance($mockClassName, $serviceProvider); + $app->register($mockClassName); + } + + /** + * @covers \Engelsystem\Application::bootstrap + * @covers \Engelsystem\Application::isBooted + */ + public function testBootstrap() + { + /** @var PHPUnit_Framework_MockObject_MockObject|Application $app */ + $app = $this->getMockBuilder(Application::class) + ->setMethods(['register']) + ->getMock(); + + $serviceProvider = $this->mockServiceProvider($app, ['boot']); + $serviceProvider->expects($this->once()) + ->method('boot'); + + $app->expects($this->once()) + ->method('register') + ->with($serviceProvider); + + $config = $this->getMockBuilder(Config::class) + ->getMock(); + + $config->expects($this->once()) + ->method('get') + ->with('providers') + ->willReturn([$serviceProvider]); + + $property = (new ReflectionClass($app))->getProperty('serviceProviders'); + $property->setAccessible(true); + $property->setValue($app, [$serviceProvider]); + + $app->bootstrap($config); + + $this->assertTrue($app->isBooted()); + + // Run bootstrap another time to ensure that providers are registered only once + $app->bootstrap($config); + } + + /** + * @param Application $app + * @param array $methods + * @return PHPUnit_Framework_MockObject_MockObject|ServiceProvider + */ + protected function mockServiceProvider(Application $app, $methods = []) + { + $serviceProvider = $this->getMockBuilder(ServiceProvider::class) + ->setConstructorArgs([$app]) + ->setMethods($methods) + ->getMockForAbstractClass(); + + return $serviceProvider; + } }