Rewrite the application using Stratify #25

Merged
mnapoli merged 4 commits from stratify into master 2016-02-21 23:01:43 +01:00
31 changed files with 1801 additions and 1427 deletions

16
.gitignore vendored
View File

@@ -1,11 +1,11 @@
/app/cache/*
!/app/cache/.gitkeep
/app/data/*
!/app/data/.gitkeep
/app/logs/*
!/app/logs/.gitkeep
/app/config/parameters.php
/.puli/
/var/cache/*
!/var/cache/.gitkeep
/var/data/*
!/var/data/.gitkeep
/var/logs/*
!/var/logs/.gitkeep
/res/config/parameters.php
/vendor/
/web/vendor/
/composer.phar
/theme/

View File

@@ -1,52 +0,0 @@
<?php
use Maintained\Application\Command\ClearCacheCommand;
use Maintained\Application\Command\ShowStatisticsCommand;
use Maintained\Application\Command\UpdateStatisticsCommand;
use Maintained\Application\Command\WarmupCacheCommand;
use Maintained\Statistics\CachedStatisticsProvider;
use Maintained\Statistics\StatisticsComputer;
use Maintained\Statistics\StatisticsProvider;
use Maintained\Statistics\StatisticsProviderLogger;
use function DI\factory;
use function DI\link;
use function DI\object;
return [
'baseUrl' => 'http://isitmaintained.com',
'maintenance' => false,
'directory.cache' => __DIR__ . '/../../app/cache',
'directory.data' => __DIR__ . '/../../app/data',
'directory.logs' => __DIR__ . '/../../app/logs',
// Routing
'routes' => require __DIR__ . '/routes.php',
// Piwik tracking
'piwik.enabled' => false,
'piwik.host' => null,
'piwik.site_id' => null,
// GitHub API
'github.auth_token' => null,
StatisticsProvider::class => link(CachedStatisticsProvider::class),
CachedStatisticsProvider::class => object()
->constructorParameter('cache', link('storage.statistics'))
->constructorParameter('wrapped', link(StatisticsProviderLogger::class)),
StatisticsProviderLogger::class => object()
->constructorParameter('wrapped', link(StatisticsComputer::class))
->constructorParameter('repositoryStorage', link('storage.repositories')),
// CLI commands
ClearCacheCommand::class => object()
->constructorParameter('cacheDirectory', link('directory.cache'))
->constructorParameter('dataDirectory', link('directory.data')),
ShowStatisticsCommand::class => object(),
WarmupCacheCommand::class => object()
->constructorParameter('repositoryStorage', link('storage.repositories')),
UpdateStatisticsCommand::class => object()
->constructorParameter('repositoryStorage', link('storage.repositories'))
->constructorParameter('statisticsCache', link('storage.statistics')),
];

View File

@@ -1,30 +0,0 @@
<?php
use Maintained\Application\Controller\BadgeController;
use Maintained\Application\Controller\Error404Controller;
use Maintained\Application\Controller\HomeController;
use Maintained\Application\Controller\ProjectCheckController;
use Maintained\Application\Controller\ProjectController;
return [
'home' => [
'pattern' => '/',
'controller' => HomeController::class,
],
'check-project' => [
'pattern' => '/check/{user}/{repository}',
'controller' => ProjectCheckController::class,
],
'project' => [
'pattern' => '/project/{user}/{repository}',
'controller' => ProjectController::class,
],
'badge' => [
'pattern' => '/badge/{badge}/{user}/{repository}.svg',
'controller' => BadgeController::class,
],
'404' => [
'pattern' => '/404',
'controller' => Error404Controller::class,
],
];

View File

@@ -1,14 +0,0 @@
<?php
use DI\ContainerBuilder;
$builder = new ContainerBuilder();
$builder->addDefinitions(__DIR__ . '/config/config.php');
$builder->addDefinitions(__DIR__ . '/config/config.libraries.php');
if (file_exists(__DIR__ . '/config/parameters.php')) {
$builder->addDefinitions(__DIR__ . '/config/parameters.php');
}
return $builder->build();

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env php
<?php
use DI\Container;
use DI\ContainerBuilder;
use Maintained\Application\Command\ClearCacheCommand;
use Maintained\Application\Command\ShowStatisticsCommand;
use Maintained\Application\Command\UpdateStatisticsCommand;
@@ -10,10 +10,24 @@ use Symfony\Component\Console\Application;
require_once __DIR__ . '/../vendor/autoload.php';
/** @var Container $container */
$container = require __DIR__ . '/../app/container.php';
$modules = [
'error-handler',
'twig',
'app',
];
/** @var \Stratify\Framework\Application $app */
$app = new class([], $modules) extends \Stratify\Framework\Application
{
protected function createContainerBuilder(array $modules) : ContainerBuilder
{
$containerBuilder = parent::createContainerBuilder($modules);
$containerBuilder->useAnnotations(true);
return $containerBuilder;
}
};
$container = $app->getContainer();
$application = new Application('Maintained');
$application = new Application('isitmaintained');
$application->add($container->get(ClearCacheCommand::class));
$application->add($container->get(WarmupCacheCommand::class));

View File

@@ -7,30 +7,23 @@
"Maintained\\": "src/Maintained/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Maintained\\": "tests/"
}
},
"require": {
"php": ">=5.6",
"php": ">=7.0",
"ext-gd": "*",
"mnapoli/php-di": "4.4.x-dev",
"php-di/php-di": "~5.1",
"knplabs/github-api": "~1.3",
"aura/router": "~2.0",
"badges/poser": "~1.1",
"twig/twig": "~1.16",
"doctrine/cache": "~1.0",
"symfony/console": "~2.6",
"symfony/filesystem": "~2.6",
"mnapoli/piwik-twig-extension": "~1.0",
"mnapoli/blackbox": "~0.4.0",
"psr/log": "~1.0",
"monolog/monolog": "~1.10"
"monolog/monolog": "~1.10",
"stratify/framework": "~0.1.2",
"stratify/twig-module": "~0.1.1",
"doctrine/annotations": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"minimum-stability": "dev",
"minimum-stability": "beta",
"prefer-stable": true
}

2560
composer.lock generated

File diff suppressed because it is too large Load Diff

184
puli.json Normal file
View File

@@ -0,0 +1,184 @@
{
"version": "1.0",
"name": "mnapoli/maintained",
"path-mappings": {
"/app": "res"
},
"config": {
"bootstrap-file": "vendor/autoload.php"
},
"packages": {
"aura/router": {
"install-path": "vendor/aura/router",
"installer": "composer"
},
"badges/poser": {
"install-path": "vendor/badges/poser",
"installer": "composer"
},
"container-interop/container-interop": {
"install-path": "vendor/container-interop/container-interop",
"installer": "composer"
},
"doctrine/annotations": {
"install-path": "vendor/doctrine/annotations",
"installer": "composer"
},
"doctrine/cache": {
"install-path": "vendor/doctrine/cache",
"installer": "composer"
},
"doctrine/lexer": {
"install-path": "vendor/doctrine/lexer",
"installer": "composer"
},
"filp/whoops": {
"install-path": "vendor/filp/whoops",
"installer": "composer"
},
"guzzle/guzzle": {
"install-path": "vendor/guzzle/guzzle",
"installer": "composer"
},
"justinrainbow/json-schema": {
"install-path": "vendor/justinrainbow/json-schema",
"installer": "composer"
},
"knplabs/github-api": {
"install-path": "vendor/knplabs/github-api",
"installer": "composer"
},
"mnapoli/blackbox": {
"install-path": "vendor/mnapoli/blackbox",
"installer": "composer"
},
"mnapoli/piwik-twig-extension": {
"install-path": "vendor/mnapoli/piwik-twig-extension",
"installer": "composer"
},
"monolog/monolog": {
"install-path": "vendor/monolog/monolog",
"installer": "composer"
},
"php-di/invoker": {
"install-path": "vendor/php-di/invoker",
"installer": "composer"
},
"php-di/php-di": {
"install-path": "vendor/php-di/php-di",
"installer": "composer"
},
"php-di/phpdoc-reader": {
"install-path": "vendor/php-di/phpdoc-reader",
"installer": "composer"
},
"psr/http-message": {
"install-path": "vendor/psr/http-message",
"installer": "composer"
},
"psr/log": {
"install-path": "vendor/psr/log",
"installer": "composer"
},
"puli/composer-plugin": {
"install-path": "vendor/puli/composer-plugin",
"installer": "composer"
},
"puli/discovery": {
"install-path": "vendor/puli/discovery",
"installer": "composer"
},
"puli/repository": {
"install-path": "vendor/puli/repository",
"installer": "composer"
},
"puli/twig-extension": {
"install-path": "vendor/puli/twig-extension",
"installer": "composer"
},
"puli/url-generator": {
"install-path": "vendor/puli/url-generator",
"installer": "composer"
},
"ramsey/uuid": {
"install-path": "vendor/ramsey/uuid",
"installer": "composer"
},
"seld/jsonlint": {
"install-path": "vendor/seld/jsonlint",
"installer": "composer"
},
"stratify/error-handler-module": {
"install-path": "vendor/stratify/error-handler-module",
"installer": "composer"
},
"stratify/framework": {
"install-path": "vendor/stratify/framework",
"installer": "composer"
},
"stratify/http": {
"install-path": "vendor/stratify/http",
"installer": "composer"
},
"stratify/router": {
"install-path": "vendor/stratify/router",
"installer": "composer"
},
"stratify/twig-module": {
"install-path": "vendor/stratify/twig-module",
"installer": "composer"
},
"symfony/console": {
"install-path": "vendor/symfony/console",
"installer": "composer"
},
"symfony/event-dispatcher": {
"install-path": "vendor/symfony/event-dispatcher",
"installer": "composer"
},
"symfony/filesystem": {
"install-path": "vendor/symfony/filesystem",
"installer": "composer"
},
"symfony/finder": {
"install-path": "vendor/symfony/finder",
"installer": "composer"
},
"symfony/polyfill-mbstring": {
"install-path": "vendor/symfony/polyfill-mbstring",
"installer": "composer"
},
"symfony/process": {
"install-path": "vendor/symfony/process",
"installer": "composer"
},
"twig/twig": {
"install-path": "vendor/twig/twig",
"installer": "composer"
},
"webmozart/assert": {
"install-path": "vendor/webmozart/assert",
"installer": "composer"
},
"webmozart/expression": {
"install-path": "vendor/webmozart/expression",
"installer": "composer"
},
"webmozart/glob": {
"install-path": "vendor/webmozart/glob",
"installer": "composer"
},
"webmozart/json": {
"install-path": "vendor/webmozart/json",
"installer": "composer"
},
"webmozart/path-util": {
"install-path": "vendor/webmozart/path-util",
"installer": "composer"
},
"zendframework/zend-diactoros": {
"install-path": "vendor/zendframework/zend-diactoros",
"installer": "composer"
}
}
}

View File

@@ -1,7 +1,5 @@
<?php
use Aura\Router\Router;
use Aura\Router\RouterFactory;
use BlackBox\Adapter\MapAdapter;
use BlackBox\Backend\FileStorage;
use BlackBox\Backend\MultipleFileStorage;
@@ -10,7 +8,6 @@ use BlackBox\Transformer\MapWithTransformers;
use BlackBox\Transformer\ObjectArrayMapper;
use BlackBox\Transformer\PhpSerializeEncoder;
use BlackBox\Transformer\StorageWithTransformers;
use DI\Container;
use Doctrine\Common\Cache\Cache;
use Doctrine\Common\Cache\FilesystemCache;
use Github\Client;
@@ -24,62 +21,42 @@ use PiwikTwigExtension\PiwikTwigExtension;
use Psr\Log\LoggerInterface;
use PUGX\Poser\Poser;
use PUGX\Poser\Render\SvgFlatRender;
use function DI\add;
use function DI\get;
use function DI\factory;
use function DI\link;
use function DI\object;
return [
ContainerInterface::class => link(Container::class),
// Routing
Router::class => factory(function (ContainerInterface $c) {
$router = (new RouterFactory())->newInstance();
// Add the routes from the array config (Aura router doesn't seem to accept routes as array)
$routes = $c->get('routes');
foreach ($routes as $routeName => $route) {
$router->add($routeName, $route['pattern'])
->addValues(['controller' => $route['controller']]);
}
return $router;
}),
// Logger
LoggerInterface::class => factory(function (ContainerInterface $c) {
LoggerInterface::class => function (ContainerInterface $c) {
$logger = new Logger('main');
$file = $c->get('directory.logs') . '/app.log';
$logger->pushHandler(new StreamHandler($file, Logger::WARNING));
return $logger;
}),
},
// Badge generator
Poser::class => object()
->constructor(link(SvgFlatRender::class)),
->constructor(get(SvgFlatRender::class)),
// Twig
Twig_Environment::class => factory(function (ContainerInterface $c) {
$loader = new Twig_Loader_Filesystem(__DIR__ . '/../../src/Maintained/Application/View');
$twig = new Twig_Environment($loader);
$twig->addExtension($c->get(TwigExtension::class));
$twig->addExtension($c->get(PiwikTwigExtension::class));
return $twig;
}),
'twig.extensions' => add([
get(TwigExtension::class),
get(PiwikTwigExtension::class),
]),
PiwikTwigExtension::class => object()
->constructor(link('piwik.host'), link('piwik.site_id'), link('piwik.enabled')),
->constructor(get('piwik.host'), get('piwik.site_id'), get('piwik.enabled')),
// Cache
Cache::class => factory(function (ContainerInterface $c) {
Cache::class => function (ContainerInterface $c) {
$cache = new FilesystemCache($c->get('directory.cache') . '/app');
$cache->setNamespace('Maintained');
return $cache;
}),
},
'storage.repositories' => factory(function (ContainerInterface $c) {
'storage.repositories' => function (ContainerInterface $c) {
$backend = new StorageWithTransformers(
new FileStorage($c->get('directory.data') . '/repositories.json')
);
@@ -89,17 +66,17 @@ return [
);
$storage->addTransformer(new ObjectArrayMapper(Repository::class));
return $storage;
}),
'storage.statistics' => factory(function (ContainerInterface $c) {
},
'storage.statistics' => function (ContainerInterface $c) {
$storage = new MapWithTransformers(
new MultipleFileStorage($c->get('directory.data') . '/statistics')
);
$storage->addTransformer(new PhpSerializeEncoder);
return $storage;
}),
},
// GitHub API
Client::class => factory(function (ContainerInterface $c) {
Client::class => function (ContainerInterface $c) {
$cacheDirectory = $c->get('directory.cache') . '/github';
$client = new Client(
@@ -112,6 +89,6 @@ return [
}
return $client;
}),
},
];

63
res/config/config.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
use function DI\add;
use Maintained\Application\Command\ClearCacheCommand;
use Maintained\Application\Command\ShowStatisticsCommand;
use Maintained\Application\Command\UpdateStatisticsCommand;
use Maintained\Application\Command\WarmupCacheCommand;
use Maintained\Application\Middleware\MaintenanceMiddleware;
use Maintained\Statistics\CachedStatisticsProvider;
use Maintained\Statistics\StatisticsComputer;
use Maintained\Statistics\StatisticsProvider;
use Maintained\Statistics\StatisticsProviderLogger;
use function DI\factory;
use function DI\get;
use function DI\object;
$config = [
'baseUrl' => 'http://isitmaintained.com',
'maintenance' => false,
'directory.cache' => __DIR__ . '/../../var/cache',
'directory.data' => __DIR__ . '/../../var/data',
'directory.logs' => __DIR__ . '/../../var/logs',
// Piwik tracking
'piwik.enabled' => false,
'piwik.host' => null,
'piwik.site_id' => null,
// GitHub API
'github.auth_token' => null,
StatisticsProvider::class => get(CachedStatisticsProvider::class),
CachedStatisticsProvider::class => object()
->constructorParameter('cache', get('storage.statistics'))
->constructorParameter('wrapped', get(StatisticsProviderLogger::class)),
StatisticsProviderLogger::class => object()
->constructorParameter('wrapped', get(StatisticsComputer::class))
->constructorParameter('repositoryStorage', get('storage.repositories')),
// CLI commands
ClearCacheCommand::class => object()
->constructorParameter('cacheDirectory', get('directory.cache'))
->constructorParameter('dataDirectory', get('directory.data')),
ShowStatisticsCommand::class => object(),
WarmupCacheCommand::class => object()
->constructorParameter('repositoryStorage', get('storage.repositories')),
UpdateStatisticsCommand::class => object()
->constructorParameter('repositoryStorage', get('storage.repositories'))
->constructorParameter('statisticsCache', get('storage.statistics')),
// Middlewares
MaintenanceMiddleware::class => object()
->constructorParameter('enabled', get('maintenance')),
];
return array_merge(
$config,
require __DIR__ . '/config.libraries.php',
require __DIR__ . '/parameters.php'
);

View File

@@ -8,6 +8,7 @@ use Maintained\Statistics\Statistics;
use Maintained\Statistics\StatisticsProvider;
use PUGX\Poser\Image;
use PUGX\Poser\Poser;
use Zend\Diactoros\Response;
/**
* @author Matthieu Napoli <matthieu@mnapoli.fr>
@@ -55,11 +56,14 @@ class BadgeController
}
}
// Cache the badge for 1 day
header('Cache-Control: max-age=86400');
header('Content-type: image/svg+xml');
$response = new Response();
$response->getBody()->write($badge);
echo $badge;
// Cache the badge for 1 day
$response = $response->withHeader('Cache-Control', 'max-age=86400');
$response = $response->withHeader('Content-type', 'image/svg+xml');
return $response;
}
/**

View File

@@ -1,17 +0,0 @@
<?php
namespace Maintained\Application\Controller;
use Twig_Environment;
/**
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
class Error404Controller
{
public function __invoke(Twig_Environment $twig)
{
header('HTTP/1.0 404 Not Found');
echo $twig->render('404.twig');
}
}

View File

@@ -45,7 +45,7 @@ class HomeController
'robbyrussell/oh-my-zsh' => 'Oh My Zsh',
];
echo $this->twig->render('home.twig', [
return $this->twig->render('/app/views/home.twig', [
'latestRepositories' => $latestRepositories,
'showcase' => $showcase,
]);

View File

@@ -1,17 +0,0 @@
<?php
namespace Maintained\Application\Controller;
use Twig_Environment;
/**
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
class MaintenanceController
{
public function __invoke(Twig_Environment $twig)
{
header('HTTP/1.0 503 Service Unavailable');
echo $twig->render('maintenance.twig');
}
}

View File

@@ -35,7 +35,7 @@ class ProjectCheckController
// Silence exception: the error will show on the badges
}
echo $this->twig->render('project-check.twig', [
return $this->twig->render('project-check.twig', [
'repository' => $user . '/' . $repository,
]);
}

View File

@@ -29,14 +29,12 @@ class ProjectController
$statistics = $this->statisticsProvider->getStatistics($user, $repository);
} catch (RuntimeException $e) {
if ($e->getMessage() === 'Not Found') {
echo $this->twig->render('not-found.twig');
return;
return $this->twig->render('/app/views/not-found.twig');
}
echo $this->twig->render('github-limit.twig');
return;
return $this->twig->render('/app/views/github-limit.twig');
}
echo $this->twig->render('project.twig', [
return $this->twig->render('/app/views/project.twig', [
'repository' => $user . '/' . $repository,
'resolutionTime' => $statistics->resolutionTime,
'openedIssues' => round($statistics->openIssuesRatio * 100),

View File

@@ -0,0 +1,31 @@
<?php
namespace Maintained\Application\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Twig_Environment;
/**
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
class Error404Middleware
{
/**
* @var Twig_Environment
*/
private $twig;
public function __construct(Twig_Environment $twig)
{
$this->twig = $twig;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response) : ResponseInterface
{
$response = $response->withStatus(404);
$response->getBody()->write($this->twig->render('/app/views/404.twig'));
return $response;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Maintained\Application\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Twig_Environment;
/**
* @author Matthieu Napoli <matthieu@mnapoli.fr>
*/
class MaintenanceMiddleware
{
/**
* @var Twig_Environment
*/
private $twig;
/**
* @var bool
*/
private $enabled;
public function __construct(Twig_Environment $twig, bool $enabled)
{
$this->twig = $twig;
$this->enabled = $enabled;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) : ResponseInterface
{
if (!$this->enabled) {
return $next($request, $response);
}
$response = $response->withStatus(503);
$response->getBody()->write($this->twig->render('/app/views/maintenance.twig'));
return $response;
}
}

View File

@@ -1,45 +1,58 @@
<?php
use Aura\Router\Router;
use DI\Container;
use Maintained\Application\Controller\Error404Controller;
use Maintained\Application\Controller\MaintenanceController;
use DI\ContainerBuilder;
use Maintained\Application\Controller\BadgeController;
use Maintained\Application\Controller\HomeController;
use Maintained\Application\Controller\ProjectCheckController;
use Maintained\Application\Controller\ProjectController;
use Maintained\Application\Middleware\Error404Middleware;
use Maintained\Application\Middleware\MaintenanceMiddleware;
use Monolog\ErrorHandler;
use Psr\Log\LoggerInterface;
use Stratify\ErrorHandlerModule\ErrorHandlerMiddleware;
use Stratify\Framework\Application;
use function Stratify\Framework\pipe;
use function Stratify\Framework\router;
use function Stratify\Router\route;
require_once __DIR__ . '/../vendor/autoload.php';
if (php_sapi_name() === 'cli-server' && is_file(__DIR__ . preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']))) {
return false;
}
/** @var Container $container */
$container = require __DIR__ . '/../app/container.php';
require __DIR__ . '/../vendor/autoload.php';
ErrorHandler::register($container->get(LoggerInterface::class));
$modules = [
'error-handler',
'twig',
'app',
];
if ($container->get('maintenance')) {
$controller = MaintenanceController::class;
$requestParameters = [];
} else {
/** @var Router $router */
$router = $container->get(Router::class);
$http = pipe([
ErrorHandlerMiddleware::class,
MaintenanceMiddleware::class,
$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$route = $router->match($url, $_SERVER);
if ($route) {
$requestParameters = $route->params;
$controller = $requestParameters['controller'];
} else {
if (php_sapi_name() === 'cli-server') {
return false;
}
router([
'/' => route(HomeController::class, 'home'),
'/check/{user}/{repository}' => route(ProjectCheckController::class, 'check-project'),
'/project/{user}/{repository}' => route(ProjectController::class, 'project'),
'/badge/{badge}/{user}/{repository}.svg' => route(BadgeController::class, 'badge'),
]),
$controller = Error404Controller::class;
$requestParameters = [];
// If no route matched
Error404Middleware::class,
]);
/** @var Application $app */
$app = new class($http, $modules) extends Application
{
protected function createContainerBuilder(array $modules) : ContainerBuilder
{
$containerBuilder = parent::createContainerBuilder($modules);
$containerBuilder->useAnnotations(true);
return $containerBuilder;
}
}
};
// Handle the case where the controller is an invokable class
if (is_string($controller) && class_exists($controller)) {
$controller = $container->make($controller);
}
ErrorHandler::register($app->getContainer()->get(LoggerInterface::class));
// Dispatch
$container->call($controller, $requestParameters);
$app->runHttp();