* uplift to latest expressive

This commit is contained in:
Danyi Dávid 2017-11-06 13:18:39 +01:00
parent b5c307166e
commit 8c5b58acd1
44 changed files with 3419 additions and 1022 deletions

139
README.md
View File

@ -1 +1,138 @@
# Skies proxy api
# Expressive Skeleton and Installer
[![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton)
[![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-skeleton/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-skeleton?branch=master)
*Begin developing PSR-7 middleware applications in seconds!*
[zend-expressive](https://github.com/zendframework/zend-expressive) builds on
[zend-stratigility](https://github.com/zendframework/zend-stratigility) to
provide a minimalist PSR-7 middleware framework for PHP with routing, DI
container, optional templating, and optional error handling capabilities.
This installer will setup a skeleton application based on zend-expressive by
choosing optional packages based on user input as demonstrated in the following
screenshot:
![screenshot-installer](https://cloud.githubusercontent.com/assets/459648/10410494/16bdc674-6f6d-11e5-8190-3c1466e93361.png)
The user selected packages are saved into `composer.json` so that everyone else
working on the project have the same packages installed. Configuration files and
templates are prepared for first use. The installer command is removed from
`composer.json` after setup succeeded, and all installer related files are
removed.
## Getting Started
Start your new Expressive project with composer:
```bash
$ composer create-project zendframework/zend-expressive-skeleton <project-path>
```
After choosing and installing the packages you want, go to the
`<project-path>` and start PHP's built-in web server to verify installation:
```bash
$ composer run --timeout=0 serve
```
You can then browse to http://localhost:8080.
> ### Setting a timeout
>
> Composer commands time out after 300 seconds (5 minutes). On Linux-based
> systems, the `php -S` command that `composer serve` spawns continues running
> as a background process, but on other systems halts when the timeout occurs.
>
> As such, we recommend running the `serve` script using a timeout. This can
> be done by using `composer run` to execute the `serve` script, with a
> `--timeout` option. When set to `0`, as in the previous example, no timeout
> will be used, and it will run until you cancel the process (usually via
> `Ctrl-C`). Alternately, you can specify a finite timeout; as an example,
> the following will extend the timeout to a full day:
>
> ```bash
> $ composer run --timeout=86400 serve
> ```
## Troubleshooting
If the installer fails during the ``composer create-project`` phase, please go
through the following list before opening a new issue. Most issues we have seen
so far can be solved by `self-update` and `clear-cache`.
1. Be sure to work with the latest version of composer by running `composer self-update`.
2. Try clearing Composer's cache by running `composer clear-cache`.
If neither of the above help, you might face more serious issues:
- Info about the [zlib_decode error](https://github.com/composer/composer/issues/4121).
- Info and solutions for [composer degraded mode](https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode).
## Application Development Mode Tool
This skeleton comes with [zf-development-mode](https://github.com/zfcampus/zf-development-mode).
It provides a composer script to allow you to enable and disable development mode.
### To enable development mode
**Note:** Do NOT run development mode on your production server!
```bash
$ composer development-enable
```
**Note:** Enabling development mode will also clear your configuration cache, to
allow safely updating dependencies and ensuring any new configuration is picked
up by your application.
### To disable development mode
```bash
$ composer development-disable
```
### Development mode status
```bash
$ composer development-status
```
## Configuration caching
By default, the skeleton will create a configuration cache in
`data/config-cache.php`. When in development mode, the configuration cache is
disabled, and switching in and out of development mode will remove the
configuration cache.
You may need to clear the configuration cache in production when deploying if
you deploy to the same directory. You may do so using the following:
```bash
$ composer clear-config-cache
```
You may also change the location of the configuration cache itself by editing
the `config/config.php` file and changing the `config_cache_path` entry of the
local `$cacheConfig` variable.
## Skeleton Development
This section applies only if you cloned this repo with `git clone`, not when you
installed expressive with `composer create-project ...`.
If you want to run tests against the installer, you need to clone this repo and
setup all dependencies with composer. Make sure you **prevent composer running
scripts** with `--no-scripts`, otherwise it will remove the installer and all
tests.
```bash
$ composer update --no-scripts
$ composer test
```
Please note that the installer tests remove installed config files and templates
before and after running the tests.
Before contributing read [the contributing guide](CONTRIBUTING.md).

View File

@ -0,0 +1,46 @@
<?php
/**
* Script for clearing the configuration cache.
*
* Can also be invoked as `composer clear-config-cache`.
*
* @see https://github.com/zendframework/zend-expressive-skeleton for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-skeleton/blob/master/LICENSE.md New BSD License
*/
chdir(__DIR__ . '/../');
require 'vendor/autoload.php';
$config = include 'config/config.php';
if (! isset($config['config_cache_path'])) {
echo "No configuration cache path found" . PHP_EOL;
exit(0);
}
if (! file_exists($config['config_cache_path'])) {
printf(
"Configured config cache file '%s' not found%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);
}
if (false === unlink($config['config_cache_path'])) {
printf(
"Error removing config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(1);
}
printf(
"Removed configured config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);

View File

@ -4,21 +4,30 @@
"type": "project",
"homepage": "https://github.com/zendframework/zend-expressive-skeleton",
"license": "BSD-3-Clause",
"config": {
"sort-packages": true
},
"require": {
"php": "^5.6 || ^7.0",
"php": "^7.1",
"doctrine/common": "2.9.x-dev",
"guzzlehttp/guzzle": "6.3.0",
"http-interop/http-middleware": "^0.4.1",
"roave/security-advisories": "dev-master",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-helpers": "^2.0",
"zendframework/zend-stdlib": "^2.7 || ^3.0",
"zendframework/zend-expressive-fastroute": "^1.0",
"zendframework/zend-servicemanager": "^2.7.3 || ^3.0",
"zendframework/zend-http": "^2.5",
"symfony/css-selector": "^3.2"
"zendframework/zend-component-installer": "^1.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-dom": "2.6.0",
"zendframework/zend-expressive": "^2.0.5",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-helpers": "^4.0",
"zendframework/zend-json": "3.0.0",
"zendframework/zend-servicemanager": "^3.3",
"zendframework/zend-stdlib": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"squizlabs/php_codesniffer": "^2.3",
"filp/whoops": "^1.1 || ^2.0"
"phpunit/phpunit": "^6.0.8 || ^5.7.15",
"squizlabs/php_codesniffer": "^2.8.1",
"zfcampus/zf-development-mode": "^3.1",
"filp/whoops": "^2.1.7"
},
"autoload": {
"psr-4": {
@ -31,10 +40,17 @@
}
},
"scripts": {
"post-create-project-cmd": [
"@development-enable"
],
"development-disable": "zf-development-mode disable",
"development-enable": "zf-development-mode enable",
"development-status": "zf-development-mode status",
"check": [
"@cs-check",
"@test"
],
"clear-config-cache": "php bin/clear-config-cache.php",
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8080 -t public public/index.php",

2280
composer.lock generated

File diff suppressed because it is too large Load Diff

1
config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
development.config.php

View File

@ -1,13 +1,21 @@
<?php
use Zend\Expressive\Application;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
use Zend\Expressive\Helper;
use Zend\Expressive\Middleware;
return [
// Provides application-wide services.
// We recommend using fully-qualified class names whenever possible as
// service names.
'dependencies' => [
// Use 'aliases' to alias a service name to another service. The
// key is the alias name, the value is the service to which it points.
'aliases' => [
'Zend\Expressive\Delegate\DefaultDelegate' => Delegate\NotFoundDelegate::class,
],
// Use 'invokables' for constructor-less services, or services that do
// not require arguments to the constructor. Map a service name to the
// class name.
@ -16,10 +24,16 @@ return [
Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class,
],
// Use 'factories' for services provided by callbacks/factory classes.
'factories' => [
Application::class => ApplicationFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
\App\Service\SkiesService::class => \App\Service\SkiesServiceFactory::class,
'factories' => [
Application::class => Container\ApplicationFactory::class,
Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
Zend\Stratigility\Middleware\ErrorHandler::class => Container\ErrorHandlerFactory::class,
Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class,
Middleware\NotFoundHandler::class => Container\NotFoundHandlerFactory::class,
],
],
];

View File

@ -0,0 +1,34 @@
<?php
/**
* Development-only configuration.
*
* Put settings you want enabled when under development mode in this file, and
* check it into your repository.
*
* Developers on your team will then automatically enable them by calling on
* `composer development-enable`.
*/
use Zend\Expressive\Container;
use Zend\Expressive\Middleware\ErrorResponseGenerator;
return [
'dependencies' => [
'invokables' => [
],
'factories' => [
ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class,
'Zend\Expressive\Whoops' => Container\WhoopsFactory::class,
'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
],
],
'whoops' => [
'json_exceptions' => [
'display' => true,
'show_trace' => true,
'ajax_only' => true,
],
],
];

View File

@ -1,7 +0,0 @@
<?php
return [
'authKey' => '',
'debug' => true,
'config_cache_enabled' => false,
];

View File

@ -0,0 +1,11 @@
<?php
/**
* Local configuration.
*
* Copy this file to `local.php` and change its settings as required.
* `local.php` is ignored by git and safe to use for local and sensitive data like usernames and passwords.
*/
return [
];

View File

@ -1,69 +0,0 @@
<?php
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
return [
'dependencies' => [
'factories' => [
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
],
],
// This can be used to seed pre- and/or post-routing middleware
'middleware_pipeline' => [
// An array of middleware to register. Each item is of the following
// specification:
//
// [
// Required:
// 'middleware' => 'Name or array of names of middleware services and/or callables',
// Optional:
// 'path' => '/path/to/match', // string; literal path prefix to match
// // middleware will not execute
// // if path does not match!
// 'error' => true, // boolean; true for error middleware
// 'priority' => 1, // int; higher values == register early;
// // lower/negative == register last;
// // default is 1, if none is provided.
// ],
//
// While the ApplicationFactory ignores the keys associated with
// specifications, they can be used to allow merging related values
// defined in multiple configuration files/locations. This file defines
// some conventional keys for middleware to execute early, routing
// middleware, and error middleware.
'always' => [
'middleware' => [
// Add more middleware here that you want to execute on
// every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
Helper\ServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
Helper\UrlHelperMiddleware::class,
// Add more middleware here that needs to introspect the routing
// results; this might include:
// - route-based authentication
// - route-based validation
// - etc.
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [
// Add error middleware here.
],
'error' => true,
'priority' => -10000,
],
],
];

View File

@ -0,0 +1,12 @@
<?php
use Zend\Expressive\Router\FastRouteRouter;
use Zend\Expressive\Router\RouterInterface;
return [
'dependencies' => [
'invokables' => [
RouterInterface::class => FastRouteRouter::class,
],
],
];

View File

@ -1,41 +0,0 @@
<?php
return [
'dependencies' => [
'invokables' => [
Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouter::class,
App\Action\PingAction::class => App\Action\PingAction::class,
],
'factories' => [
App\Action\HomePageAction::class => App\Action\HomePageFactory::class,
App\Action\ActivityAction::class => App\Action\ActivityFactory::class,
],
],
'routes' => [
[
'name' => 'home',
'path' => '/',
'middleware' => App\Action\HomePageAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'api.ping',
'path' => '/api/ping',
'middleware' => App\Action\PingAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'api.activity',
'path' => '/api/activity',
'middleware' => App\Action\ActivityAction::class,
'allowed_methods' => ['GET', 'POST', 'DELETE', 'OPTIONS'],
],
[
'name' => 'api.activity.id',
'path' => '/api/activity/{id:\d+}',
'middleware' => App\Action\ActivityAction::class,
'allowed_methods' => ['GET', 'PUT', 'DELETE', 'OPTIONS'],
],
],
];

View File

@ -1,11 +1,24 @@
<?php
use Zend\ConfigAggregator\ConfigAggregator;
return [
// Toggle the configuration cache. Set this to boolean false, or remove the
// directive, to disable configuration caching. Toggling development mode
// will also disable it by default; clear the configuration cache using
// `composer clear-config-cache`.
ConfigAggregator::ENABLE_CACHE => true,
// Enable debugging; typically used to provide debugging information within templates.
'debug' => false,
'config_cache_enabled' => false,
'zend-expressive' => [
// Enable programmatic pipeline: Any `middleware_pipeline` or `routes`
// configuration will be ignored when creating the `Application` instance.
'programmatic_pipeline' => true,
// Provide templates for the error handling middleware to use when
// generating responses.
'error_handler' => [
'template_404' => 'error::404',
'template_error' => 'error::error',

View File

@ -1,35 +1,32 @@
<?php
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\Glob;
use Zend\ConfigAggregator\ArrayProvider;
use Zend\ConfigAggregator\ConfigAggregator;
use Zend\ConfigAggregator\PhpFileProvider;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
* then ``local.php`` and finally ``*.local.php``. This way local settings overwrite global settings.
*
* The configuration can be cached. This can be done by setting ``config_cache_enabled`` to ``true``.
*
* Obviously, if you use closures in your config you can't cache it.
*/
// To enable or disable caching, set the `ConfigAggregator::ENABLE_CACHE` boolean in
// `config/autoload/local.php`.
$cacheConfig = [
'config_cache_path' => 'data/config-cache.php',
];
$cachedConfigFile = 'data/cache/app_config.php';
$aggregator = new ConfigAggregator([
// Include cache configuration
new ArrayProvider($cacheConfig),
$config = [];
if (is_file($cachedConfigFile)) {
// Try to load the cached config
$config = include $cachedConfigFile;
} else {
// Load configuration from autoload path
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
$config = ArrayUtils::merge($config, include $file);
}
// Default App module config
App\ConfigProvider::class,
// Cache config if enabled
if (isset($config['config_cache_enabled']) && $config['config_cache_enabled'] === true) {
file_put_contents($cachedConfigFile, '<?php return ' . var_export($config, true) . ';');
}
}
// Load application config in a pre-defined order in such a way that local settings
// overwrite global settings. (Loaded as first to last):
// - `global.php`
// - `*.global.php`
// - `local.php`
// - `*.local.php`
new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
// Return an ArrayObject so we can inject the config as a service in Aura.Di
// and still use array checks like ``is_array``.
return new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS);
// Load development config if it exists
new PhpFileProvider('config/development.config.php'),
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();

View File

@ -0,0 +1,29 @@
<?php
/**
* File required to allow enablement of development mode.
*
* For use with the zf-development-mode tool.
*
* Usage:
* $ composer development-disable
* $ composer development-enable
* $ composer development-status
*
* DO NOT MODIFY THIS FILE.
*
* Provide your own development-mode settings by editing the file
* `config/autoload/development.local.php.dist`.
*
* Because this file is aggregated last, it simply ensures:
*
* - The `debug` flag is _enabled_.
* - Configuration caching is _disabled_.
*/
use Zend\ConfigAggregator\ConfigAggregator;
return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
];

55
config/pipeline.php Normal file
View File

@ -0,0 +1,55 @@
<?php
use Zend\Expressive\Helper\ServerUrlMiddleware;
use Zend\Expressive\Helper\UrlHelperMiddleware;
use Zend\Expressive\Middleware\ImplicitHeadMiddleware;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Middleware\NotFoundHandler;
use Zend\Stratigility\Middleware\ErrorHandler;
/**
* Setup middleware pipeline:
*/
// The error handler should be the first (most outer) middleware to catch
// all Exceptions.
$app->pipe(ErrorHandler::class);
$app->pipe(ServerUrlMiddleware::class);
// Pipe more middleware here that you want to execute on every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
//
// Piped Middleware may be either callables or service names. Middleware may
// also be passed as an array; each item in the array must resolve to
// middleware eventually (i.e., callable or service name).
//
// Middleware can be attached to specific paths, allowing you to mix and match
// applications under a common domain. The handlers in each middleware
// attached this way will see a URI with the MATCHED PATH SEGMENT REMOVED!!!
//
// - $app->pipe('/api', $apiMiddleware);
// - $app->pipe('/docs', $apiDocMiddleware);
// - $app->pipe('/files', $filesMiddleware);
// Register the routing middleware in the middleware pipeline
$app->pipeRoutingMiddleware();
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
// Add more middleware here that needs to introspect the routing results; this
// might include:
//
// - route-based authentication
// - route-based validation
// - etc.
// Register the dispatch middleware in the middleware pipeline
$app->pipeDispatchMiddleware();
// At this point, if no Response is return by any middleware, the
// NotFoundHandler kicks in; alternately, you can provide other fallback
// middleware to execute.
$app->pipe(NotFoundHandler::class);

34
config/routes.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* Setup routes with a single request method:
*
* $app->get('/', App\Action\HomePageAction::class, 'home');
* $app->post('/album', App\Action\AlbumCreateAction::class, 'album.create');
* $app->put('/album/:id', App\Action\AlbumUpdateAction::class, 'album.put');
* $app->patch('/album/:id', App\Action\AlbumUpdateAction::class, 'album.patch');
* $app->delete('/album/:id', App\Action\AlbumDeleteAction::class, 'album.delete');
*
* Or with multiple request methods:
*
* $app->route('/contact', App\Action\ContactAction::class, ['GET', 'POST', ...], 'contact');
*
* Or handling all request methods:
*
* $app->route('/contact', App\Action\ContactAction::class)->setName('contact');
*
* or:
*
* $app->route(
* '/contact',
* App\Action\ContactAction::class,
* Zend\Expressive\Router\Route::HTTP_METHOD_ANY,
* 'contact'
* );
*/
$app->get('/', App\Action\HomePageAction::class, 'home');
$app->get('/api/ping', App\Action\PingAction::class, 'api.ping');
$app->get('/api/activity[/{id:\d+}]', App\Action\ActivityAction::class, 'api.activity.get');
$app->get('/api/activity/signup/{id:\d+}', App\Action\ActivitySignupAction::class, 'api.activity.signup');
$app->get('/api/activity/signoff/{id:\d+}', App\Action\ActivitySignoffAction::class, 'api.activity.signoff');

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<ruleset name="Zend Framework coding standard">
<description>Zend Framework coding standard</description>
<ruleset name="Expressive Skeleton coding standard">
<description>Expressive Skeleton coding standard</description>
<!-- display progress -->
<arg value="p"/>

View File

@ -10,9 +10,20 @@ if (php_sapi_name() === 'cli-server'
chdir(dirname(__DIR__));
require 'vendor/autoload.php';
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
/**
* Self-called anonymous function that creates its own scope and keep the global namespace clean.
*/
call_user_func(function () {
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Zend\Expressive\Application $app */
$app = $container->get(\Zend\Expressive\Application::class);
$app->run();
/** @var \Zend\Expressive\Application $app */
$app = $container->get(\Zend\Expressive\Application::class);
// Import programmatic/declarative middleware pipeline and routing
// configuration statements
require 'config/pipeline.php';
require 'config/routes.php';
$app->run();
});

View File

@ -2,16 +2,17 @@
namespace App\Action;
use Psr\Http\Message\ResponseInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Stratigility\MiddlewareInterface;
use Zend\Json\Json;
abstract class AbstractAction implements MiddlewareInterface
abstract class AbstractAction implements ServerMiddlewareInterface
{
const IDENTIFIER_NAME = 'id';
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$requestMethod = strtoupper($request->getMethod());
$id = $request->getAttribute(static::IDENTIFIER_NAME);
@ -19,68 +20,68 @@ abstract class AbstractAction implements MiddlewareInterface
switch ($requestMethod) {
case 'GET':
return isset($id)
? $this->get($request, $response, $next)
: $this->getList($request, $response, $next);
? $this->get($request, $delegate)
: $this->getList($request, $delegate);
case 'POST':
return $this->create($request, $response, $next);
return $this->create($request, $delegate);
case 'PUT':
return $this->update($request, $response, $next);
return $this->update($request, $delegate);
case 'DELETE':
return isset($id)
? $this->delete($request, $response, $next)
: $this->deleteList($request, $response, $next);
? $this->delete($request, $delegate)
: $this->deleteList($request, $delegate);
case 'HEAD':
return $this->head($request, $response, $next);
return $this->head($request, $delegate);
case 'OPTIONS':
return $this->options($request, $response, $next);
return $this->options($request, $delegate);
case 'PATCH':
return $this->patch($request, $response, $next);
return $this->patch($request, $delegate);
default:
return $next($request, $response);
return $delegate->process($request);
}
}
public function get(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function get(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function getList(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function getList(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function create(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function create(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function update(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function update(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function delete(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function delete(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function deleteList(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function deleteList(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function head(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function head(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function options(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function options(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function patch(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function patch(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
@ -89,4 +90,69 @@ abstract class AbstractAction implements MiddlewareInterface
{
return new JsonResponse($data, $status);
}
/**
*
* @param ServerRequestInterface $request
* @return array|object
*/
public function getRequestData(ServerRequestInterface $request)
{
$body = $request->getParsedBody();
if (!empty($body)) {
return $body;
}
return $this->parseRequestData(
$request->getBody()->getContents(),
$request->getHeaderLine('content-type')
);
}
/**
*
* @param string $input
* @param string $contentType
* @return mixed
*/
private function parseRequestData($input, $contentType)
{
$contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
$parser = $this->returnParserContentType($contentTypeParts[0]);
return $parser($input);
}
/**
*
* @param string $contentType
* @return callable
*/
private function returnParserContentType(string $contentType)
{
if ($contentType === 'application/x-www-form-urlencoded') {
return function ($input) {
parse_str($input, $data);
return $data;
};
} elseif ($contentType === 'application/json') {
return function ($input) {
$jsonDecoder = new Json();
try {
return $jsonDecoder->decode($input, Json::TYPE_ARRAY);
} catch (\Exception $e) {
return false;
}
};
} elseif ($contentType === 'multipart/form-data') {
return function ($input) {
return $input;
};
}
return function ($input) {
return $input;
};
}
}

View File

@ -2,33 +2,34 @@
namespace App\Action;
use App\Service\SkiesService;
use Psr\Http\Message\ResponseInterface;
use App\Service\SkiesClientService;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Response\TextResponse;
class ActivityAction extends AbstractAction
{
private $skiesService;
/**
* @var SkiesClientService
*/
private $skiesClient;
public function __construct(SkiesService $skiesService)
public function __construct(SkiesClientService $skiesClient)
{
$this->skiesService = $skiesService;
$this->skiesClient = $skiesClient;
}
public function getList(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function getList(ServerRequestInterface $request, DelegateInterface $delegate)
{
return new JsonResponse($this->skiesService->getActivityList());
$authHeader = $request->getHeaderLine("x-passthru-auth");
return new JsonResponse($this->skiesClient->setAuthHeader($authHeader)->getActivities());
}
public function get(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function get(ServerRequestInterface $request, DelegateInterface $delegate)
{
$id = $request->getAttribute(self::IDENTIFIER_NAME);
return new JsonResponse($this->skiesService->getActivity($id));
}
public function options(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
return new JsonResponse(true);
$authHeader = $request->getHeaderLine("x-passthru-auth");
return new JsonResponse($this->skiesClient->setAuthHeader($authHeader)->getActivity($id));
}
}

View File

@ -2,14 +2,14 @@
namespace App\Action;
use App\Service\SkiesService;
use App\Service\SkiesClientService;
use Interop\Container\ContainerInterface;
class ActivityFactory
{
public function __invoke(ContainerInterface $container)
{
$skiesService = $container->get(SkiesService::class);
return new ActivityAction($skiesService);
$skiesClient = $container->get(SkiesClientService::class);
return new ActivityAction($skiesClient);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class ActivitySignoffAction implements ServerMiddlewareInterface
{
/**
* @var SkiesClientService
*/
private $skiesClient;
public function __construct(SkiesClientService $skiesClient)
{
$this->skiesClient = $skiesClient;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$authHeader = $request->getHeaderLine("x-passthru-auth");
$id = $request->getAttribute("id");
return new JsonResponse($this->skiesClient->setAuthHeader($authHeader)->signOffActivity($id));
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
use Interop\Container\ContainerInterface;
class ActivitySignoffFactory
{
public function __invoke(ContainerInterface $container)
{
$skiesClient = $container->get(SkiesClientService::class);
return new ActivitySignoffAction($skiesClient);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class ActivitySignupAction implements ServerMiddlewareInterface
{
/**
* @var SkiesClientService
*/
private $skiesClient;
public function __construct(SkiesClientService $skiesClient)
{
$this->skiesClient = $skiesClient;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$authHeader = $request->getHeaderLine("x-passthru-auth");
$id = $request->getAttribute("id");
return new JsonResponse($this->skiesClient->setAuthHeader($authHeader)->signUpActivity($id));
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
use Interop\Container\ContainerInterface;
class ActivitySignupFactory
{
public function __invoke(ContainerInterface $container)
{
$skiesClient = $container->get(SkiesClientService::class);
return new ActivitySignupAction($skiesClient);
}
}

View File

@ -2,7 +2,8 @@
namespace App\Action;
use Psr\Http\Message\ResponseInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
@ -12,7 +13,7 @@ use Zend\Expressive\Plates\PlatesRenderer;
use Zend\Expressive\Twig\TwigRenderer;
use Zend\Expressive\ZendView\ZendViewRenderer;
class HomePageAction
class HomePageAction implements ServerMiddlewareInterface
{
private $router;
@ -24,8 +25,15 @@ class HomePageAction
$this->template = $template;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
if (! $this->template) {
return new JsonResponse([
'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
'docsUrl' => 'https://docs.zendframework.com/zend-expressive/',
]);
}
$data = [];
if ($this->router instanceof Router\AuraRouter) {
@ -36,7 +44,7 @@ class HomePageAction
$data['routerDocs'] = 'https://github.com/nikic/FastRoute';
} elseif ($this->router instanceof Router\ZendRouter) {
$data['routerName'] = 'Zend Router';
$data['routerDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.mvc.routing.html';
$data['routerDocs'] = 'https://docs.zendframework.com/zend-router/';
}
if ($this->template instanceof PlatesRenderer) {
@ -47,14 +55,7 @@ class HomePageAction
$data['templateDocs'] = 'http://twig.sensiolabs.org/documentation';
} elseif ($this->template instanceof ZendViewRenderer) {
$data['templateName'] = 'Zend View';
$data['templateDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.view.quick-start.html';
}
if (!$this->template) {
return new JsonResponse([
'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
'docsUrl' => 'zend-expressive.readthedocs.org',
]);
$data['templateDocs'] = 'https://docs.zendframework.com/zend-view/';
}
return new HtmlResponse($this->template->render('app::home-page', $data));

View File

@ -11,7 +11,7 @@ class HomePageFactory
public function __invoke(ContainerInterface $container)
{
$router = $container->get(RouterInterface::class);
$template = ($container->has(TemplateRendererInterface::class))
$template = $container->has(TemplateRendererInterface::class)
? $container->get(TemplateRendererInterface::class)
: null;

View File

@ -0,0 +1,18 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
class NewsAction
{
/**
* @var SkiesClientService
*/
private $skiesClient;
public function __construct(SkiesClientService $skiesClient)
{
$this->skiesClient = $skiesClient;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Action;
use App\Service\SkiesClientService;
use Interop\Container\ContainerInterface;
class NewsFactory
{
public function __invoke(ContainerInterface $container)
{
$skiesClient = $container->get(SkiesClientService::class);
return new NewsAction($skiesClient);
}
}

View File

@ -2,16 +2,15 @@
namespace App\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Zend\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class PingAction
class PingAction implements ServerMiddlewareInterface
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
return new JsonResponse([
'ack' => time(),
]);
return new JsonResponse(['ack' => time()]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App;
/**
* The configuration provider for the App module
*
* @see https://docs.zendframework.com/zend-component-installer/
*/
class ConfigProvider
{
/**
* Returns the configuration array
*
* To add a bit of a structure, each section is defined in a separate
* method which returns an array with its configuration.
*
* @return array
*/
public function __invoke()
{
return [
'dependencies' => $this->getDependencies(),
'templates' => $this->getTemplates(),
];
}
/**
* Returns the container dependencies
*
* @return array
*/
public function getDependencies()
{
return [
'invokables' => [
Action\PingAction::class => Action\PingAction::class,
],
'factories' => [
Action\HomePageAction::class => Action\HomePageFactory::class,
Action\ActivityAction::class => Action\ActivityFactory::class,
Action\ActivitySignupAction::class => Action\ActivitySignupFactory::class,
Action\ActivitySignoffAction::class => Action\ActivitySignoffFactory::class,
Action\NewsAction::class => Action\NewsFactory::class,
Service\SkiesClientService::class => Service\SkiesClientServiceFactory::class,
],
];
}
/**
* Returns the templates configuration
*
* @return array
*/
public function getTemplates()
{
return [
'paths' => [
'app' => ['templates/app'],
'error' => ['templates/error'],
'layout' => ['templates/layout'],
],
];
}
}

305
src/App/Entity/Activity.php Normal file
View File

@ -0,0 +1,305 @@
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Activity implements \JsonSerializable
{
/**
* @var int
*/
private $id;
/**
* @var string
*/
private $name;
/**
* @var \DateTime
*/
private $date;
/**
* @var string
*/
private $description;
/**
* @var \DateTime
*/
private $finalEntry;
/**
* @var string
*/
private $accountable;
/**
* @var ArrayCollection|Comment[]
*/
private $comments;
/**
* @var string[]
*/
private $signedUsers;
/**
* @var bool
*/
private $isSignedup = false;
/**
* Can sign up or sign off activity
* @var bool
*/
private $canChangeSignup = false;
public function __construct()
{
$this->comments = new ArrayCollection();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
* @return Activity
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string $name
* @return Activity
*/
public function setName(?string $name): Activity
{
$this->name = $name;
return $this;
}
/**
* @return \DateTime
*/
public function getDate(): ?\DateTime
{
return $this->date;
}
/**
* @param \DateTime $date
* @return Activity
*/
public function setDate(?\DateTime $date): Activity
{
$this->date = $date;
return $this;
}
/**
* @return string
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string $description
* @return Activity
*/
public function setDescription(?string $description): Activity
{
$this->description = $description;
return $this;
}
/**
* @return \DateTime
*/
public function getFinalEntry(): ?\DateTime
{
return $this->finalEntry;
}
/**
* @param \DateTime $finalEntry
* @return Activity
*/
public function setFinalEntry(\DateTime $finalEntry): Activity
{
$this->finalEntry = $finalEntry;
return $this;
}
/**
* @return string
*/
public function getAccountable(): ?string
{
return $this->accountable;
}
/**
* @param string $accountable
* @return Activity
*/
public function setAccountable(string $accountable): Activity
{
$this->accountable = $accountable;
return $this;
}
/**
* @return Comment[]|ArrayCollection
*/
public function getComments()
{
return $this->comments;
}
/**
* @param Comment[]|ArrayCollection $comments
* @return Activity
*/
public function setComments($comments): Activity
{
$this->comments = $comments;
return $this;
}
/**
* @param Comment $comment
* @return Activity
*/
public function addComment(Comment $comment): Activity
{
if(!$this->comments->contains($comment)) {
$this->comments->add($comment);
}
return $this;
}
/**
* @param Comment $comment
* @return Activity
*/
public function removeComment(Comment $comment): Activity
{
if($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
}
return $this;
}
/**
* @return string[]
*/
public function getSignedUsers(): ?array
{
return $this->signedUsers;
}
/**
* @param string[] $signedUsers
* @return Activity
*/
public function setSignedUsers(array $signedUsers): Activity
{
$this->signedUsers = $signedUsers;
return $this;
}
/**
* @param string $signed
* @return Activity
*/
public function addSignedUser(string $signed): Activity
{
$this->signedUsers[] = $signed;
return $this;
}
/**
* @return bool
*/
public function isSignedup(): bool
{
return $this->isSignedup;
}
/**
* @param bool $isSignedup
* @return Activity
*/
public function setIsSignedup(bool $isSignedup): Activity
{
$this->isSignedup = $isSignedup;
return $this;
}
/**
* @return bool
*/
public function isCanChangeSignup(): bool
{
return $this->canChangeSignup;
}
/**
* @param bool $canChangeSignup
* @return Activity
*/
public function setCanChangeSignup(bool $canChangeSignup): Activity
{
$this->canChangeSignup = $canChangeSignup;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'id' => $this->getId(),
'name' => $this->getName(),
'date' => $this->getDate()
? $this->getDate()->format("Y-m-d H:i:s")
: null,
'description' => $this->getDescription(),
'finalEntry' => $this->getFinalEntry()
? $this->getFinalEntry()->format("Y-m-d H:i:s")
: null,
'accountable' => $this->getAccountable(),
'comments' => $this->getComments()->getValues(),
'signedUsers' => $this->getSignedUsers(),
'canChangeSignup' => $this->isCanChangeSignup(),
'isSignedUp' => $this->isSignedup()
];
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Entity;
class Comment implements \JsonSerializable
{
/**
* @var string
*/
private $text;
/**
* @var \DateTime
*/
private $createdAt;
/**
* @var User
*/
private $user;
/**
* @return string
*/
public function getText(): string
{
return $this->text;
}
/**
* @param string $text
* @return Comment
*/
public function setText(string $text): Comment
{
$this->text = $text;
return $this;
}
/**
* @return \DateTime
*/
public function getCreatedAt(): \DateTime
{
return $this->createdAt;
}
/**
* @param \DateTime $createdAt
* @return Comment
*/
public function setCreatedAt(\DateTime $createdAt): Comment
{
$this->createdAt = $createdAt;
return $this;
}
/**
* @return User
*/
public function getUser(): User
{
return $this->user;
}
/**
* @param User $user
* @return Comment
*/
public function setUser(User $user): Comment
{
$this->user = $user;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'text' => $this->getText(),
'user' => $this->getUser(),
'createdAt' => $this->getCreatedAt()
? $this->getCreatedAt()->format("Y-m-d H:i:s")
: null,
];
}
}

8
src/App/Entity/News.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace App\Entity;
class News
{
}

67
src/App/Entity/User.php Normal file
View File

@ -0,0 +1,67 @@
<?php
namespace App\Entity;
class User implements \JsonSerializable
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $displayName;
/**
* @return string
*/
public function getUsername(): string
{
return $this->username;
}
/**
* @param string $username
* @return User
*/
public function setUsername(string $username): User
{
$this->username = $username;
return $this;
}
/**
* @return string
*/
public function getDisplayName(): string
{
return $this->displayName;
}
/**
* @param string $displayName
* @return User
*/
public function setDisplayName(string $displayName): User
{
$this->displayName = $displayName;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'username' => $this->getUsername(),
'displayName' => $this->getDisplayName(),
];
}
}

View File

@ -0,0 +1,397 @@
<?php
namespace App\Service;
use App\Entity\Activity;
use App\Entity\Comment;
use App\Entity\User;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use Zend\Dom\Document;
use Zend\Expressive\Exception\MissingDependencyException;
class SkiesClientService
{
const SKIES_MAIN_URL = "https://skies.sigmatechnology.se/main.asp";
const SKIES_PROFILE_URL = "https://skies.sigmatechnology.se/main.asp?rID=1&alt=2&username=%s";
const SKIES_ACTIVITIES_URL = "https://skies.sigmatechnology.se/main.asp?rID=2";
const SKIES_ACTIVITY_URL = "https://skies.sigmatechnology.se/main.asp?rID=2&alt=1&aktID=%s";
const SKIES_ACTIVITY_SIGNUP_URL = "https://skies.sigmatechnology.se/main.asp?rID=2&alt=1&aktID=%s&doJoin=1";
const SKIES_ACTIVITY_SIGNOFF_URL = "https://skies.sigmatechnology.se/main.asp?rID=2&alt=1&aktID=%s&doCancel=1&user=%s";
/**
* @var Client
*/
private $client;
/**
* @var string
*/
private $authHeader = null;
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* @param string $authHeader
* @return SkiesClientService
*/
public function setAuthHeader(string $authHeader): SkiesClientService
{
$this->authHeader = $authHeader;
return $this;
}
public function getNews()
{
$response = $this->doSkiesRequest('GET', self::SKIES_MAIN_URL);
$htmlBody = $response->getBody();
return $this->parseMainPage($htmlBody);
}
/**
* @return Activity[]
*/
public function getActivities()
{
$response = $this->doSkiesRequest('GET', self::SKIES_ACTIVITIES_URL);
$htmlBody = $response->getBody();
return $this->parseActivitiesPage($htmlBody);
}
/**
* @param string $htmlBody
* @return Activity[]
*/
private function parseActivitiesPage(string $htmlBody)
{
$domDocument = new Document($htmlBody);
$eventNodes = Document\Query::execute(
"div.container div.row div.box > table tr",
$domDocument,
Document\Query::TYPE_CSS
);
$activities = [];
for ($i = 1; $i < $eventNodes->count(); $i++) {
$eventRow = $eventNodes[$i];
$rowCells = Document\Query::execute(
"./td",
$domDocument,
Document\Query::TYPE_XPATH,
$eventRow
);
$href = $rowCells[0]->childNodes->item(0)->getAttribute("href");
$queryString = parse_url($href, PHP_URL_QUERY);
parse_str($queryString, $queryParams);
$activities[] = $this->getActivity($queryParams["aktID"]);
}
return $activities;
}
/**
* @param int $id
* @return Activity
*/
public function getActivity(int $id): Activity
{
$response = $this->doSkiesRequest('GET', sprintf(self::SKIES_ACTIVITY_URL, $id));
$htmlBody = $response->getBody();
return $this->parseActivityPage($htmlBody, $id);
}
/**
* @param int $id
* @return Activity
*/
public function signUpActivity(int $id): Activity
{
$this->doSkiesRequest('POST', sprintf(self::SKIES_ACTIVITY_SIGNUP_URL, $id), [
'form_params' => [
'user' => $this->getUsername(),
],
]);
return $this->getActivity($id);
}
/**
* @param int $id
* @return Activity
*/
public function signOffActivity(int $id): Activity
{
$username = $this->getUsername();
$this->doSkiesRequest('GET', sprintf(self::SKIES_ACTIVITY_SIGNOFF_URL, $id, $username));
return $this->getActivity($id);
}
/**
* @param string $htmlBody
* @param int $id
* @return Activity
*/
private function parseActivityPage(string $htmlBody, int $id): Activity
{
$domDocument = new Document($htmlBody);
/** Activity body */
$activityNode = Document\Query::execute(
"div.col-md-10 div.box",
$domDocument,
Document\Query::TYPE_CSS
);
/** Activity name */
$h5Nodes = Document\Query::execute(
"./h5",
$domDocument,
Document\Query::TYPE_XPATH,
$activityNode[0]
);
$divNodes = Document\Query::execute(
".//div",
$domDocument,
Document\Query::TYPE_XPATH,
$activityNode[0]
);
/** Comment block */
$commentNodes = Document\Query::execute(
'.//div[@class="onecomment"]',
$domDocument,
Document\Query::TYPE_XPATH,
$activityNode[0]
);
/** Signed users block */
$signedUserParagraph = Document\Query::execute(
'//div[@class="portlet"]//div[@class="paragraph"]/text()',
$domDocument,
Document\Query::TYPE_XPATH
);
preg_match("#Date:.*?([0-9]{4}-[0-9]{2}-[0-9]{2}).*?Time: ([0-9]{1,2}:[0-9]{2}).*?Final entry day:.*?([0-9]{4}-[0-9]{2}-[0-9]{2}).*?Accountable: (.*?) / ([0-9 +]*)#msi", $divNodes[1]->textContent, $matches);
$signedUsers = $this->parseActivitySignedUsers($signedUserParagraph);
$isSignedUp = in_array($this->getDisplayName($this->getUsername()), $signedUsers);
$canChangeSignup = $isSignedUp
? $this->canSignOff($domDocument, $activityNode[0])
: $this->canSignup($domDocument, $activityNode[0]);
$activity = new Activity();
$activity->setId($id)
->setName($this->clearTextNode($h5Nodes[0]->textContent))
->setDescription($this->parseActivityDescription($activityNode[0]))
->setDate(new \DateTime(sprintf("%s %s", $matches[1], $matches[2])))
->setFinalEntry(new \DateTime($matches[3]))
->setAccountable(str_replace(
" ",
" ",
$matches[5]
? sprintf("%s (%s)", $matches[4], $matches[5])
: $matches[4])
)
->setSignedUsers($signedUsers)
->setIsSignedup($isSignedUp)
->setCanChangeSignup($canChangeSignup);
$this->parseActivityComments($commentNodes, $activity);
return $activity;
}
/**
* @param \DOMNode $element
* @return null|string
*/
private function parseActivityDescription(\DOMNode $element): ?string
{
$description = "";
$isContent = false;
/** @var \DOMElement $childNode */
foreach ($element->childNodes as $childNode) {
if ($childNode->nodeName == "hr") {
$isContent = true;
}
if ($childNode->nodeName == "form") {
break;
}
if ($childNode->nodeName == "span") {
break;
}
if ($childNode->nodeName == "script") {
break;
}
if ($isContent) {
$description .= $childNode->nodeName == "br"
? "\n"
: rtrim($childNode->textContent);
}
}
return $this->clearTextNode($description);
}
/**
* @param Document\NodeList $commentElements
* @param Activity $activity
*/
private function parseActivityComments(Document\NodeList $commentElements, Activity $activity)
{
/** @var \DOMElement $commentElement */
foreach ($commentElements as $commentElement) {
$divElements = $commentElement->getElementsByTagName("div");
$queryString = parse_url(
$commentElement->getElementsByTagName("a")->item(0)->getAttribute("href"),
PHP_URL_QUERY
);
parse_str($queryString, $queryParams);
preg_match(
"#(.*)\s/\s([0-9]{4}-[0-9]{2}-[0-9]{2})\s([0-9]{1,2}:[0-9]{1,2})#msi",
str_replace(" ", " ", $divElements->item(2)->textContent),
$matches
);
$user = new User();
$user->setDisplayName($matches[1])
->setUsername($queryParams['username']);
$comment = new Comment();
$comment
->setText($divElements->item(1)->textContent)
->setUser($user)
->setCreatedAt(new \DateTime(sprintf(
"%s %s",
$matches[2],
$matches[3]
)));
$activity->addComment($comment);
}
}
/**
* @param Document\NodeList $usersBlockText
* @return array
* @todo remove unsign button if it is present
*/
private function parseActivitySignedUsers(Document\NodeList $usersBlockText): array
{
$signed = [];
for ($i = 1; $i < $usersBlockText->count(); $i++) {
preg_match(
"#[0-9]+\.[^\p{L}]([\p{L}\s]+)#msiu",
$usersBlockText[$i]->textContent,
$usernameMatch
);
$signed[] = $usernameMatch[1];
}
$collator = new \Collator("hu_HU");
$collator->sort($signed);
return $signed;
}
/**
* Can sign off if there is no red span inside the node
* @param Document $domDocument
* @param \DOMNode $domNode
* @return bool
*/
private function canSignOff(Document $domDocument, \DOMNode $domNode): bool
{
$redSpanNodes = Document\Query::execute(
'./span[@class="red"]',
$domDocument,
Document\Query::TYPE_XPATH,
$domNode
);
return $redSpanNodes->count() == 0;
}
/**
* Can sign up if there is a form direct descendant
* @param Document $domDocument
* @param \DOMNode $domNode
* @return bool
*/
private function canSignUp(Document $domDocument, \DOMNode $domNode): bool
{
$formNodes = Document\Query::execute(
"./form",
$domDocument,
Document\Query::TYPE_XPATH,
$domNode
);
return $formNodes->count() == 1;
}
private function parseMainPage(string $htmlBody)
{
return false;
}
private function getDisplayName(string $username): string
{
$response = $this->doSkiesRequest("GET", sprintf(self::SKIES_PROFILE_URL, $username));
$profilePage = $response->getBody();
$domDocument = new Document($profilePage);
$h1Nodes = Document\Query::execute(
'//div[@class="box"]/h1',
$domDocument,
Document\Query::TYPE_XPATH
);
return $this->clearTextNode($h1Nodes[0]->textContent);
}
/**
* Clear junk from text nodes
*
* @param string $text
* @return string
*/
private function clearTextNode(string $text): string
{
$text = str_replace(" ", " ", $text);
$text = str_replace("–", "-", $text);
$text = preg_replace("#[ \t]+#msiu", " ", $text);
return trim($text, " \t\n\r\0\x0B" . chr(0xC2) . chr(0xA0));
}
/**
* Do an http request adding the Authorization header
*
* @param string $method
* @param string $url
* @param array $options
* @return ResponseInterface
*/
private function doSkiesRequest(string $method, string $url, $options = []): ResponseInterface
{
if ($this->authHeader == null) {
throw new MissingDependencyException("X-Passthru-Auth header is missing");
}
return $this->client
->request($method, $url, [
'headers' => [
'Authorization' => "Basic {$this->authHeader}",
]
] + $options);
}
/**
* @return string
*/
private function getUsername(): string
{
if (null == $this->authHeader) {
throw new MissingDependencyException("X-Passthru-Auth header is missing");
}
$decodedHeader = base64_decode($this->authHeader);
list($username) = explode(":", $decodedHeader);
return $username;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Service;
use GuzzleHttp\Client;
use Interop\Container\ContainerInterface;
class SkiesClientServiceFactory
{
public function __invoke(ContainerInterface $container)
{
$httpClient = new Client([
'cookies' => true,
]);
return new SkiesClientService($httpClient);
}
}

View File

@ -1,187 +0,0 @@
<?php
namespace App\Service;
use Interop\Container\ContainerInterface;
use Symfony\Component\CssSelector\CssSelectorConverter;
use Zend\Http\Client;
/**
* Class SkiesService
*
* @package App\Service
* @todo handle errors in http response
*/
class SkiesService
{
const BASE_URI = 'http://skies.sigmatechnology.se/';
/**
* @var Client
*/
private $httpClient;
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->httpClient = new Client();
$headers = $this->httpClient->getRequest()->getHeaders();
$headers->addHeaderLine('Authorization', 'Basic ' . $this->getAuthToken());
}
/**
* Get a list of all activities
*
* @return array
*/
public function getActivityList()
{
$this->httpClient->setUri(self::BASE_URI . "main.asp?rID=2");
$skiesResponse = $this->httpClient->send();
$skiesHtmlBody = $skiesResponse->getBody();
$cssToXpathConverter = new CssSelectorConverter();
$xpathQuery = $cssToXpathConverter->toXPath('html > body > div > div.row > div.col-md-10 > div.row > div.col-md-9 > div.box > table.table-striped > tr');
$doc = new \DOMDocument();
$doc->loadHTML($skiesHtmlBody);
$xpath = new \DOMXPath($doc);
$elements = $xpath->query($xpathQuery);
$parsed = [];
/** @var \DOMNode $domElement */
foreach ($elements as $domElement) {
if($domElement->childNodes->item(0)->nodeName != 'td') {
continue;
}
$url = $domElement->childNodes->item(0)->childNodes->item(0)->attributes->getNamedItem('href')->textContent;
preg_match("/aktid=([0-9]+)/msi", $url, $match);
$parsed[] = [
'id' => (int)$match[1],
'label' => $domElement->childNodes->item(0)->childNodes->item(0)->textContent,
'date' => $domElement->childNodes->item(2)->textContent,
'time' => trim($domElement->childNodes->item(3)->textContent, " \t\n\r\0\x0B\xc2\xa0"),
];
}
return $parsed;
}
/**
* Get a single activity
*
* @param int $id
* @return array
*/
public function getActivity(int $id)
{
$this->httpClient->setUri(self::BASE_URI . "main.asp?rID=2&alt=1&aktID=" . $id);
$skiesResponse = $this->httpClient->send();
$skiesHtmlBody = $skiesResponse->getBody();
$cssToXpathConverter = new CssSelectorConverter();
$xpathQuery = $cssToXpathConverter->toXPath('div.container > div.row > div.col-md-10 > div.row > div.col-md-9 > div.box');
$xpathCommentsQuery = $cssToXpathConverter->toXPath('div.container div.paragraph > div.onecomment');
$doc = new \DOMDocument();
$doc->loadHTML($skiesHtmlBody);
$xpath = new \DOMXPath($doc);
$elements = $xpath->query($xpathQuery);
$h5Elements = $elements->item(0)->getElementsByTagName('h5');
$title = $h5Elements->item(0)->textContent;
$detailDivElements = $elements->item(0)->getElementsByTagName('div');
$commentElements = $xpath->query($xpathCommentsQuery);
$eventDetails = $this->parseActivityDetails($detailDivElements->item(1)->textContent);
$eventDescription = $this->parseActivityDescription($elements->item(0));
$eventComments = $this->parseActivityComments($commentElements);
return [
'title' => $title,
'details' => $eventDetails,
'description' => $eventDescription,
'comments' => $eventComments,
];
}
/**
* @return null|string
* @todo real auth token from whatever...
*/
private function getAuthToken(): ?string
{
$config = $this->container->get('config');
return $config['authKey'];
}
/**
* Parse the activity information details returning the date, time finaly entry and the person accountable
*
* @param string $divText
* @return array|null
*/
private function parseActivityDetails(string $divText): ?array
{
preg_match("#Date:.*?([0-9]{4}-[0-9]{2}-[0-9]{2}).*?Time: ([0-9]{1,2}:[0-9]{2}).*?Final entry day:.*?([0-9]{4}-[0-9]{2}-[0-9]{2}).*?Accountable: (.*?) / ([0-9 +]*)#msi", $divText, $matches);
return [
'date' => $matches[1],
'time' => $matches[2],
'finalEntry' => $matches[3],
'accountable' => str_replace(" ", " ", $matches[5] ? sprintf("%s (%s)", $matches[4], $matches[5]) : $matches[4]),
];
}
/**
* Parses comment block. Comment section is pretty much unformatted text
* between a <hr> tag and a <form> tag in the passed $element
*
* @param \DOMElement $element
* @return null|string
*/
private function parseActivityDescription(\DOMElement $element): ?string
{
$description = "";
$isContent = false;
/** @var \DOMElement $childNode */
foreach($element->childNodes as $childNode) {
if ($childNode->nodeName == "hr") { $isContent = true; }
if ($childNode->nodeName == "form") { break; }
if ($isContent) {
$description .= $childNode->nodeName == "br"
? "\n"
: rtrim($childNode->textContent);
}
}
return trim($description);
}
/**
* Parse $commentElements
*
* @param \DOMNodeList $commentElements
* @return array|null
*/
private function parseActivityComments(\DOMNodeList $commentElements): ?array
{
$comments = [];
/** @var \DOMElement $commentElement */
foreach($commentElements as $commentElement) {
preg_match("#(.*)\s/\s([0-9]{4}-[0-9]{2}-[0-9]{2})\s([0-9]{1,2}:[0-9]{1,2})#msi", str_replace(" "," ", $commentElement->getElementsByTagName("div")->item(2)->textContent), $matches);
$comments[] = [
'comment' => $commentElement->getElementsByTagName("div")->item(1)->textContent,
'user' => $matches[1],
'date' => $matches[2],
'time' => $matches[3],
];
}
return $comments;
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Service;
use Interop\Container\ContainerInterface;
class SkiesServiceFactory
{
public function __invoke(ContainerInterface $container)
{
return new SkiesService($container);
}
}

View File

View File

@ -3,11 +3,16 @@
namespace AppTest\Action;
use App\Action\HomePageAction;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageActionTest extends \PHPUnit_Framework_TestCase
class HomePageActionTest extends TestCase
{
/** @var RouterInterface */
protected $router;
@ -17,12 +22,31 @@ class HomePageActionTest extends \PHPUnit_Framework_TestCase
$this->router = $this->prophesize(RouterInterface::class);
}
public function testResponse()
public function testReturnsJsonResponseWhenNoTemplateRendererProvided()
{
$homePage = new HomePageAction($this->router->reveal(), null);
$response = $homePage(new ServerRequest(['/']), new Response(), function () {
});
$response = $homePage->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$this->assertTrue($response instanceof Response);
$this->assertInstanceOf(JsonResponse::class, $response);
}
public function testReturnsHtmlResponseWhenTemplateRendererProvided()
{
$renderer = $this->prophesize(TemplateRendererInterface::class);
$renderer
->render('app::home-page', Argument::type('array'))
->willReturn('');
$homePage = new HomePageAction($this->router->reveal(), $renderer->reveal());
$response = $homePage->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$this->assertInstanceOf(HtmlResponse::class, $response);
}
}

View File

@ -5,10 +5,11 @@ namespace AppTest\Action;
use App\Action\HomePageAction;
use App\Action\HomePageFactory;
use Interop\Container\ContainerInterface;
use PHPUnit\Framework\TestCase;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageFactoryTest extends \PHPUnit_Framework_TestCase
class HomePageFactoryTest extends TestCase
{
/** @var ContainerInterface */
protected $container;
@ -26,11 +27,11 @@ class HomePageFactoryTest extends \PHPUnit_Framework_TestCase
$factory = new HomePageFactory();
$this->container->has(TemplateRendererInterface::class)->willReturn(false);
$this->assertTrue($factory instanceof HomePageFactory);
$this->assertInstanceOf(HomePageFactory::class, $factory);
$homePage = $factory($this->container->reveal());
$this->assertTrue($homePage instanceof HomePageAction);
$this->assertInstanceOf(HomePageAction::class, $homePage);
}
public function testFactoryWithTemplate()
@ -41,10 +42,10 @@ class HomePageFactoryTest extends \PHPUnit_Framework_TestCase
->get(TemplateRendererInterface::class)
->willReturn($this->prophesize(TemplateRendererInterface::class));
$this->assertTrue($factory instanceof HomePageFactory);
$this->assertInstanceOf(HomePageFactory::class, $factory);
$homePage = $factory($this->container->reveal());
$this->assertTrue($homePage instanceof HomePageAction);
$this->assertInstanceOf(HomePageAction::class, $homePage);
}
}

View File

@ -3,20 +3,24 @@
namespace AppTest\Action;
use App\Action\PingAction;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class PingActionTest extends \PHPUnit_Framework_TestCase
class PingActionTest extends TestCase
{
public function testResponse()
{
$pingAction = new PingAction();
$response = $pingAction(new ServerRequest(['/']), new Response(), function () {
});
$response = $pingAction->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$json = json_decode((string) $response->getBody());
$this->assertTrue($response instanceof Response);
$this->assertTrue($response instanceof Response\JsonResponse);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertTrue(isset($json->ack));
}
}