Core/src/App.php

482 lines
18 KiB
PHP
Raw Normal View History

2020-02-24 12:34:31 +00:00
<?php
2020-06-12 08:09:02 +00:00
namespace Benzine;
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
use Benzine\ORM\Laminator;
2020-06-12 08:09:02 +00:00
use Benzine\Services\ConfigurationService;
2020-06-12 13:57:36 +00:00
use Benzine\Services\EnvironmentService;
use Benzine\Services\SessionService;
use Benzine\Twig\Extensions;
2020-02-24 12:34:31 +00:00
use Cache\Adapter\Apc\ApcCachePool;
use Cache\Adapter\Apcu\ApcuCachePool;
use Cache\Adapter\Chain\CachePoolChain;
use Cache\Adapter\PHPArray\ArrayCachePool;
2020-06-12 13:57:36 +00:00
use Cache\Adapter\Redis\RedisCachePool;
2020-02-24 12:34:31 +00:00
use DebugBar\Bridge\MonologCollector;
use DebugBar\DebugBar;
use DebugBar\StandardDebugBar;
2020-06-12 13:57:36 +00:00
use DI\Container;
use DI\ContainerBuilder;
2020-02-24 12:34:31 +00:00
use Faker\Factory as FakerFactory;
use Faker\Provider;
2020-06-12 13:57:36 +00:00
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Logger;
2020-02-24 21:34:42 +00:00
use Monolog\Processor\PsrLogMessageProcessor;
2020-06-12 13:57:36 +00:00
use Psr\Container\ContainerInterface;
2020-02-24 12:34:31 +00:00
use Psr\Http\Message\ResponseInterface;
use Slim;
2020-06-12 13:57:36 +00:00
use Slim\Factory\AppFactory;
use Symfony\Component\HttpFoundation\Session\Session;
use Twig;
use Twig\Loader\FilesystemLoader;
2020-06-12 08:09:02 +00:00
class App
2020-02-24 12:34:31 +00:00
{
public const DEFAULT_TIMEZONE = 'Europe/London';
2020-06-12 13:57:36 +00:00
private static bool $isInitialised = false;
public static App $instance;
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
protected EnvironmentService $environmentService;
protected ConfigurationService $configurationService;
protected \Slim\App $app;
protected Logger $logger;
protected bool $isSessionsEnabled = true;
protected array $routePaths = [];
protected array $viewPaths = [];
protected bool $interrogateControllersComplete = false;
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
public function __construct()
2020-02-24 12:34:31 +00:00
{
2020-06-12 13:57:36 +00:00
// Configure Dependency Injector
$container = $this->setupContainer();
AppFactory::setContainer($container);
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
// Configure Router
2020-02-24 12:34:31 +00:00
$this->routePaths = [
2020-06-12 13:57:36 +00:00
APP_ROOT.'/src/Routes.php',
APP_ROOT.'/src/RoutesExtra.php',
2020-02-24 12:34:31 +00:00
];
2020-06-12 13:57:36 +00:00
$this->setup($container);
// Configure Slim
$this->app = AppFactory::create();
$this->app->add(Slim\Views\TwigMiddleware::createFromContainer($this->app));
$this->app->addRoutingMiddleware();
$errorMiddleware = $this->app->addErrorMiddleware(true, true, true);
2020-02-24 12:34:31 +00:00
}
2020-06-12 13:57:36 +00:00
protected function setup(ContainerInterface $container): void
2020-02-24 12:34:31 +00:00
{
2020-06-12 13:57:36 +00:00
$this->logger = $container->get(Logger::class);
2020-02-24 12:34:31 +00:00
if ('cli' != php_sapi_name() && $this->isSessionsEnabled) {
2020-06-12 13:57:36 +00:00
$session = $container->get(SessionService::class);
}
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$this->setupMiddlewares($container);
$this->viewPaths[] = APP_ROOT.'/views/';
$this->viewPaths[] = APP_ROOT.'/src/Views/';
$this->interrogateControllers();
2020-02-24 12:34:31 +00:00
}
2020-06-12 13:57:36 +00:00
public function setupContainer(): Container
2020-02-24 12:34:31 +00:00
{
2020-06-15 06:19:42 +00:00
$app = $this;
2020-06-12 13:57:36 +00:00
$container =
(new ContainerBuilder())
->useAutowiring(true)
->useAnnotations(true)
#->enableCompilation(APP_ROOT . "/cache")
#->writeProxiesToFile(true, APP_ROOT . "/cache/injection-proxies")
->build();
$container->set(Slim\Views\Twig::class, function(ContainerInterface $container) {
2020-02-24 12:34:31 +00:00
foreach ($this->viewPaths as $i => $viewLocation) {
if (!file_exists($viewLocation) || !is_dir($viewLocation)) {
unset($this->viewPaths[$i]);
}
}
2020-06-12 13:57:36 +00:00
$settings = ['cache' => 'cache/twig'];
$loader = new FilesystemLoader();
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
foreach ($this->viewPaths as $path) {
$loader->addPath($path);
}
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$twig = new Slim\Views\Twig($loader, $settings);
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$twig->addExtension(new Extensions\ArrayUniqueTwigExtension());
$twig->addExtension(new Extensions\FilterAlphanumericOnlyTwigExtension());
2020-02-24 12:34:31 +00:00
// Add coding string transform filters (ie: camel_case to StudlyCaps)
2020-06-12 13:57:36 +00:00
$twig->addExtension(new Extensions\TransformExtension());
2020-02-24 12:34:31 +00:00
// Add pluralisation/depluralisation support with singularize/pluralize filters
2020-06-12 13:57:36 +00:00
$twig->addExtension(new Extensions\InflectionExtension());
2020-02-24 12:34:31 +00:00
// Added Twig_Extension_Debug to enable twig dump() etc.
2020-06-12 13:57:36 +00:00
$twig->addExtension(new Twig\Extension\DebugExtension());
$twig->offsetSet('app_name', APP_NAME);
$twig->offsetSet('year', date('Y'));
return $twig;
});
$container->set('view', function(ContainerInterface $container){
return $container->get(Slim\Views\Twig::class);
});
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$container->set(EnvironmentService::class, function(ContainerInterface $container){
return new EnvironmentService();
});
2020-02-24 12:34:31 +00:00
2020-06-15 06:19:42 +00:00
$container->set(ConfigurationService::class, function(ContainerInterface $container) use ($app){
return new ConfigurationService(
$app,
$container->get(EnvironmentService::class)
);
2020-06-12 13:57:36 +00:00
});
2020-06-15 06:19:42 +00:00
2020-06-12 13:57:36 +00:00
$container->set(\Faker\Generator::class, function(ContainerInterface $c) {
2020-02-24 12:34:31 +00:00
$faker = FakerFactory::create();
$faker->addProvider(new Provider\Base($faker));
$faker->addProvider(new Provider\DateTime($faker));
$faker->addProvider(new Provider\Lorem($faker));
$faker->addProvider(new Provider\Internet($faker));
$faker->addProvider(new Provider\Payment($faker));
$faker->addProvider(new Provider\en_US\Person($faker));
$faker->addProvider(new Provider\en_US\Address($faker));
$faker->addProvider(new Provider\en_US\PhoneNumber($faker));
$faker->addProvider(new Provider\en_US\Company($faker));
return $faker;
2020-06-12 13:57:36 +00:00
});
$container->set(CachePoolChain::class, function(ContainerInterface $c) {
2020-02-24 12:34:31 +00:00
$caches = [];
// If apc/apcu present, add it to the pool
if (function_exists('apcu_add')) {
$caches[] = new ApcuCachePool();
} elseif (function_exists('apc_add')) {
$caches[] = new ApcCachePool();
}
// If Redis is configured, add it to the pool.
2020-06-12 13:57:36 +00:00
$caches[] = new RedisCachePool($c->get(\Redis::class));
2020-02-24 12:34:31 +00:00
$caches[] = new ArrayCachePool();
return new CachePoolChain($caches);
2020-06-12 13:57:36 +00:00
});
$container->set('MonologFormatter', function(ContainerInterface $c) {
2020-02-24 12:34:31 +00:00
/** @var Services\EnvironmentService $environment */
$environment = $c->get(Services\EnvironmentService::class);
return
new LineFormatter(
// the default output format is "[%datetime%] %channel%.%level_name%: %message% %context% %extra%"
$environment->get('MONOLOG_FORMAT', '[%datetime%] %channel%.%level_name%: %message% %context% %extra%')."\n",
'Y n j, g:i a'
2020-06-12 13:57:36 +00:00
);
});
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$container->set(Logger::class, function(ContainerInterface $c) {
/** @var ConfigurationService $configuration */
$configuration = $c->get(ConfigurationService::class);
2020-02-24 21:34:42 +00:00
2020-06-12 13:57:36 +00:00
$monolog = new Logger($configuration->get(ConfigurationService::KEY_APP_NAME));
$monolog->pushHandler(new ErrorLogHandler(), Logger::DEBUG);
2020-02-24 21:34:42 +00:00
$monolog->pushProcessor(new PsrLogMessageProcessor());
return $monolog;
2020-06-12 13:57:36 +00:00
});
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$container->set(DebugBar::class, function(ContainerInterface $container) {
2020-02-24 12:34:31 +00:00
$debugBar = new StandardDebugBar();
/** @var Logger $logger */
2020-06-12 13:57:36 +00:00
$logger = $container->get(Logger::class);
$debugBar->addCollector(new MonologCollector($logger));
2020-02-24 12:34:31 +00:00
return $debugBar;
2020-06-12 13:57:36 +00:00
});
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$container->set(\Middlewares\Debugbar::class, function(ContainerInterface $container) {
2020-02-24 12:34:31 +00:00
$debugBar = $container->get(DebugBar::class);
return new \Middlewares\Debugbar($debugBar);
2020-06-12 13:57:36 +00:00
});
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$container->set(\Redis::class, function(ContainerInterface $container){
$environmentService = $container->get(EnvironmentService::class);
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
$redis = new \Redis();
$redis->connect(
$environmentService->get('REDIS_HOST', 'redis'),
$environmentService->get('REDIS_PORT', 6379)
);
2020-02-24 12:34:31 +00:00
2020-06-12 13:57:36 +00:00
return $redis;
});
$container->set(SessionService::class, function(ContainerInterface $container){
return new SessionService(
$container->get(\Redis::class)
);
});
$container->set(Laminator::class, function(ContainerInterface $container){
return new Laminator(
APP_ROOT,
$container->get(ConfigurationService::class)
);
});
2020-02-24 12:34:31 +00:00
/** @var Services\EnvironmentService $environmentService */
2020-06-12 13:57:36 +00:00
$environmentService = $container->get(Services\EnvironmentService::class);
if ($environmentService->has('TIMEZONE')) {
2020-02-24 12:34:31 +00:00
date_default_timezone_set($environmentService->get('TIMEZONE'));
2020-05-24 16:03:18 +00:00
} elseif (file_exists('/etc/timezone')) {
date_default_timezone_set(trim(file_get_contents('/etc/timezone')));
2020-02-24 12:34:31 +00:00
} else {
date_default_timezone_set(self::DEFAULT_TIMEZONE);
}
2020-06-12 13:57:36 +00:00
$debugBar = $container->get(DebugBar::class);
return $container;
2020-02-24 12:34:31 +00:00
}
2020-06-12 13:57:36 +00:00
public function setupMiddlewares(ContainerInterface $container): void
2020-02-24 12:34:31 +00:00
{
// Middlewares
2020-06-12 13:57:36 +00:00
#$this->app->add($container->get(Middleware\EnvironmentHeadersOnResponse::class));
#$this->app->add($container->get(\Middlewares\ContentLength::class));
#$this->app->add($container->get(\Middlewares\Debugbar::class));
#$this->app->add($container->get(\Middlewares\Geolocation::class));
#$this->app->add($container->get(\Middlewares\TrailingSlash::class));
#$this->app->add($container->get(Middleware\JSONResponseLinter::class));
#$this->app->add($container->get(\Middlewares\Whoops::class));
#$this->app->add($container->get(\Middlewares\Minifier::class));
#$this->app->add($container->get(\Middlewares\GzipEncoder::class));
2020-02-24 12:34:31 +00:00
}
/**
* @param mixed $doNotUseStaticInstance
*
* @return self
*/
public static function Instance(array $options = [])
{
2020-06-12 13:57:36 +00:00
if (!self::$isInitialised) {
2020-02-24 12:34:31 +00:00
$calledClass = get_called_class();
self::$instance = new $calledClass($options);
}
return self::$instance;
}
public function getApp()
{
return $this->app;
}
public function addRoutePath($path): self
{
if (file_exists($path)) {
$this->routePaths[] = $path;
}
return $this;
}
public function clearRoutePaths(): self
{
$this->routePaths = [];
return $this;
}
public function addViewPath($path)
{
if (file_exists($path)) {
$this->viewPaths[] = $path;
}
return $this;
}
public function makeClean(): self
{
$this->setup();
$this->loadAllRoutes();
return $this;
}
public static function Log(int $level = Logger::DEBUG, $message)
{
return self::Instance()
->getContainer()
->get(Log\Logger::class)
->log($level, ($message instanceof \Exception) ? $message->__toString() : $message)
;
}
public function loadAllRoutes()
{
$app = $this->getApp();
foreach ($this->routePaths as $path) {
if (file_exists($path)) {
include $path;
}
}
Router\Router::Instance()->populateRoutes($app);
return $this;
}
public static function waitForMySQLToBeReady($connection = null)
{
if (!$connection) {
/** @var DbConfig $configs */
$dbConfig = self::Instance()->getContainer()->get(DatabaseConfig::class);
$configs = $dbConfig->__toArray();
if (isset($configs['Default'])) {
$connection = $configs['Default'];
} else {
foreach ($configs as $option => $connection) {
self::waitForMySQLToBeReady($connection);
}
return;
}
}
$ready = false;
echo "Waiting for MySQL ({$connection['hostname']}:{$connection['port']}) to come up...";
while (false == $ready) {
$conn = @fsockopen($connection['hostname'], $connection['port']);
if (is_resource($conn)) {
fclose($conn);
$ready = true;
} else {
echo '.';
usleep(500000);
}
}
echo " [DONE]\n";
/** @var Services\EnvironmentService $environmentService */
$environmentService = self::Container()->get(Services\EnvironmentService::class);
$environmentService->rebuildEnvironmentVariables();
}
2020-06-12 13:57:36 +00:00
public function runHttp(): void
2020-02-24 12:34:31 +00:00
{
2020-06-12 13:57:36 +00:00
$this->app->run();
2020-02-24 12:34:31 +00:00
}
protected function interrogateControllers()
{
if ($this->interrogateControllersComplete) {
return;
}
$this->interrogateControllersComplete = true;
$controllerPaths = [
2020-06-12 13:57:36 +00:00
APP_ROOT . '/src/Controllers',
2020-02-24 12:34:31 +00:00
];
2020-06-12 13:57:36 +00:00
2020-02-24 12:34:31 +00:00
foreach ($controllerPaths as $controllerPath) {
//$this->logger->debug("Route Discovery - {$controllerPath}");
if (file_exists($controllerPath)) {
foreach (new \DirectoryIterator($controllerPath) as $controllerFile) {
if (!$controllerFile->isDot() && $controllerFile->isFile() && $controllerFile->isReadable()) {
//$this->logger->debug(" > {$controllerFile->getPathname()}");
2020-06-12 13:57:36 +00:00
$appClass = new \ReflectionClass(get_called_class());
2020-02-24 12:34:31 +00:00
$expectedClasses = [
$appClass->getNamespaceName().'\\Controllers\\'.str_replace('.php', '', $controllerFile->getFilename()),
'⌬\\Controllers\\'.str_replace('.php', '', $controllerFile->getFilename()),
];
foreach ($expectedClasses as $expectedClass) {
//$this->logger->debug(" > {$expectedClass}");
if (class_exists($expectedClass)) {
$rc = new \ReflectionClass($expectedClass);
if (!$rc->isAbstract()) {
foreach ($rc->getMethods() as $method) {
/** @var \ReflectionMethod $method */
2020-06-12 13:57:36 +00:00
if (true || ResponseInterface::class == ($method->getReturnType() instanceof \ReflectionType ? $method->getReturnType()->getName() : null)) {
2020-02-24 12:34:31 +00:00
$docBlock = $method->getDocComment();
foreach (explode("\n", $docBlock) as $docBlockRow) {
if (false === stripos($docBlockRow, '@route')) {
continue;
}
//$this->logger->debug(" > fff {$docBlockRow}");
$route = trim(substr(
$docBlockRow,
(stripos($docBlockRow, '@route') + strlen('@route'))
));
//$this->logger->debug(" > Route {$route}");
//\Kint::dump($route);
@list($httpMethods, $path, $extra) = explode(' ', $route, 3);
//\Kint::dump($httpMethods, $path, $extra);exit;
$httpMethods = explode(',', strtoupper($httpMethods));
$options = [];
$defaultOptions = [
'access' => Router\Route::ACCESS_PUBLIC,
'weight' => 100,
];
if (isset($extra)) {
foreach (explode(' ', $extra) as $item) {
@list($extraK, $extraV) = explode('=', $item, 2);
if (!isset($extraV)) {
$extraV = true;
}
$options[$extraK] = $extraV;
}
}
$options = array_merge($defaultOptions, $options);
foreach ($httpMethods as $httpMethod) {
//$this->logger->debug(" > Adding {$path} to router");
$newRoute = Router\Route::Factory()
->setHttpMethod($httpMethod)
->setRouterPattern('/'.ltrim($path, '/'))
->setCallback($method->class.':'.$method->name)
;
foreach ($options as $key => $value) {
2020-04-01 16:21:36 +00:00
$keyMethod = 'set'.ucfirst($key);
2020-02-24 12:34:31 +00:00
if (method_exists($newRoute, $keyMethod)) {
$newRoute->{$keyMethod}($value);
2020-04-01 16:21:36 +00:00
} else {
$newRoute->setArgument($key, $value);
2020-02-24 12:34:31 +00:00
}
}
Router\Router::Instance()->addRoute($newRoute);
}
}
}
}
}
}
}
}
}
}
}
Router\Router::Instance()->weighRoutes();
}
}