Browse Source

add-front

pull/2/head
fedy95 5 years ago
parent
commit
84f77efcbe
  1. 8
      README.md
  2. 14
      composer.json
  3. 1639
      composer.lock
  4. 2
      config/bundles.php
  5. 11
      config/packages/dev/monolog.yaml
  6. 7
      config/packages/prod/deprecations.yaml
  7. 16
      config/packages/prod/monolog.yaml
  8. 12
      config/packages/test/monolog.yaml
  9. 2
      config/packages/test/twig.yaml
  10. 2
      config/packages/twig.yaml
  11. 1
      config/services.yaml
  12. 2
      devops/dist/.env-dist
  13. 4
      devops/docker/.env
  14. 4
      devops/docker/php/Dockerfile
  15. 1
      devops/docker/redis/redis.conf
  16. 29
      docker-compose.yaml
  17. 42
      src/Controller/IndexController.php
  18. 24
      src/Form/WeatherFormType.php
  19. 57
      src/Service/Endpoint/WeatherHistory.php
  20. 14
      src/Service/Endpoint/WeatherHistoryInterface.php
  21. 59
      symfony.lock
  22. 66
      templates/base.html.twig
  23. 10
      tests/_support/ApiTester.php
  24. 9
      tests/_support/Helper/Api.php
  25. 2
      tests/acceptance.suite.yml
  26. 60
      tests/acceptance/Controller/IndexControllerCest.php
  27. 23
      tests/acceptance/Controller/PageControllerCest.php
  28. 8
      tests/api.suite.yml
  29. 36
      tests/api/Controller/PageControllerCest.php

8
README.md

@ -1,10 +1,6 @@
# symfony-5-cli-template
# weather_site
Template repo "pure-skeleton" with symfony 5.2. Includes:
- local devops via docker-compose and make
- codeception support with acceptance/api/unit-testing
- php/yaml linters
- php/composer code style fixers
Front for a weather_history project
### Requirements

14
composer.json

@ -6,10 +6,14 @@
"ext-apcu": "5.1.*",
"ext-ctype": "7.4.*",
"ext-iconv": "7.4.*",
"ext-json": "*",
"symfony/console": "5.2.*",
"symfony/dotenv": "5.2.*",
"symfony/flex": "^1.3.1",
"symfony/form": "5.2.*",
"symfony/framework-bundle": "5.2.*",
"symfony/monolog-bundle": "^3.6",
"symfony/twig-bundle": "5.2.*",
"symfony/yaml": "5.2.*"
},
"replace": {
@ -82,10 +86,8 @@
"parallel-lint --no-progress --no-colors --blame ./src",
"parallel-lint --no-progress --no-colors --blame ./tests/_support/Helper",
"parallel-lint --no-progress --no-colors --blame ./tests/_support/AcceptanceTester.php",
"parallel-lint --no-progress --no-colors --blame ./tests/_support/ApiTester.php",
"parallel-lint --no-progress --no-colors --blame ./tests/_support/UnitTester.php",
"parallel-lint --no-progress --no-colors --blame ./tests/acceptance",
"parallel-lint --no-progress --no-colors --blame ./tests/api",
"parallel-lint --no-progress --no-colors --blame ./tests/unit",
"parallel-lint --no-progress --no-colors --blame ./.php_cs"
],
@ -95,7 +97,6 @@
"bin/console --quiet --no-debug l:yaml ./config/routes.yaml",
"bin/console --quiet --no-debug l:yaml ./config/services.yaml",
"bin/console --quiet --no-debug l:yaml ./tests/acceptance.suite.yml",
"bin/console --quiet --no-debug l:yaml ./tests/api.suite.yml",
"bin/console --quiet --no-debug l:yaml ./tests/codeception.yml",
"bin/console --quiet --no-debug l:yaml ./tests/unit.suite.yml"
],
@ -107,10 +108,8 @@
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache src",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/_support/Helper",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/_support/AcceptanceTester.php",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/_support/ApiTester.php",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/_support/UnitTester.php",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/acceptance",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/api",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache tests/unit",
"php vendor/bin/php-cs-fixer fix --cache-file=var/php-cs-fixer/.php-cs.cache .php_cs"
],
@ -121,17 +120,12 @@
"@lint:container",
"@php-cs-fix",
"@test:unit",
"@test:api",
"@test:acceptance"
],
"test:acceptance": [
"@codecept build -q",
"@codecept run acceptance"
],
"test:api": [
"@codecept build -q",
"@codecept run api"
],
"test:unit": [
"@codecept build -q",
"@codecept run unit --no-rebuild --coverage-html --coverage-text --no-colors"

1639
composer.lock
File diff suppressed because it is too large
View File

2
config/bundles.php

@ -2,4 +2,6 @@
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
];

11
config/packages/dev/monolog.yaml

@ -0,0 +1,11 @@
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]

7
config/packages/prod/deprecations.yaml

@ -0,0 +1,7 @@
monolog:
channels: [deprecation]
handlers:
deprecation:
type: stream
channels: [deprecation]
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"

16
config/packages/prod/monolog.yaml

@ -0,0 +1,16 @@
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]

12
config/packages/test/monolog.yaml

@ -0,0 +1,12 @@
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug

2
config/packages/test/twig.yaml

@ -0,0 +1,2 @@
twig:
strict_variables: true

2
config/packages/twig.yaml

@ -0,0 +1,2 @@
twig:
default_path: '%kernel.project_dir%/templates'

1
config/services.yaml

@ -1,4 +1,5 @@
parameters:
weather_history_server: '%env(string:WEATHER_HISTORY_SERVER)%'
services:
_defaults:

2
devops/dist/.env-dist

@ -1,4 +1,4 @@
APP_ENV=dev
APP_SECRET=a1def1573d162b7d97ed994df9d37949
DATABASE_URL=mysql://root:rootroot@mysql:3306/template?serverVersion=5.7
WEATHER_HISTORY_SERVER=http://weather_history_nginx_1:80

4
devops/docker/.env

@ -1,5 +1,5 @@
COMPOSE_PROJECT_NAME=template
PROJECT_ID=001
COMPOSE_PROJECT_NAME=weather_site
PROJECT_ID=003
COMPOSE_DOCKER_CLI_BUILD=1
DOCKER_BUILDKIT=1

4
devops/docker/php/Dockerfile

@ -15,9 +15,7 @@ RUN docker-php-ext-install zip
RUN apt-get install -y git
RUN docker-php-ext-install \
pcntl \
mysqli \
pdo_mysql
pcntl
# APCU
RUN pecl install apcu-5.1.17 && docker-php-ext-enable apcu

1
devops/docker/redis/redis.conf

@ -1 +0,0 @@
bind 0.0.0.0

29
docker-compose.yaml

@ -1,24 +1,10 @@
version: "3"
services:
mysql:
image: mysql:5.7
restart: on-failure
networks:
weather_history_gate:
external: true
environment:
MYSQL_USER: ${MYSQL_USER}
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: ${COMPOSE_PROJECT_NAME}
ports:
- "3$PROJECT_ID:3306"
redis:
image: redis:6.0.10-alpine
volumes:
- ./devops/docker/redis/redis.conf:/redis.conf
command: [ "redis-server", "/redis.conf" ]
ports:
- "6$PROJECT_ID:6379"
services:
php:
build:
context: devops/docker/php
@ -28,9 +14,6 @@ services:
UID: "$UID"
GID: "$GID"
restart: unless-stopped
depends_on:
- mysql
- redis
environment:
- COMPOSER_MEMORY_LIMIT=-1
- PHP_IDE_CONFIG=serverName=${COMPOSE_PROJECT_NAME}
@ -42,6 +25,8 @@ services:
- ${HOME}/.ssh:${HOME}/.ssh
expose:
- "9000"
networks:
- weather_history_gate
nginx:
image: nginx:1.19
restart: unless-stopped
@ -52,3 +37,5 @@ services:
- ./devops/docker/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
ports:
- "8$PROJECT_ID:80"
networks:
- weather_history_gate

42
src/Controller/IndexController.php

@ -4,19 +4,47 @@ declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Form\WeatherFormType;
use App\Service\Endpoint\WeatherHistoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class IndexController
class IndexController extends AbstractController
{
private WeatherHistoryInterface $weatherHistory;
public function __construct(WeatherHistoryInterface $weatherHistory)
{
$this->weatherHistory = $weatherHistory;
}
/**
* @Route("/", name="index.any")
*/
public function index(): JsonResponse
public function index(Request $request): \Symfony\Component\HttpFoundation\Response
{
return new JsonResponse(
JsonResponse::$statusTexts[JsonResponse::HTTP_BAD_REQUEST],
JsonResponse::HTTP_BAD_REQUEST
);
$weatherForm = null;
$weather = null;
$history = null;
try {
$form = $this->createForm(WeatherFormType::class);
$form->handleRequest($request);
$weatherForm = $form->createView();
if ($form->isSubmitted() && $form->isValid()) {
$weather = $this->weatherHistory->getByDate($form->getData()['date']);
}
$history = $this->weatherHistory->getHistory();
} catch (\Exception $e) {
} finally {
return $this->render('base.html.twig', [
'weatherForm' => $weatherForm,
'weather' => $weather,
'history' => $history,
]);
}
}
}

24
src/Form/WeatherFormType.php

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class WeatherFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('date', DateType::class, [
'widget' => 'single_text',
'format' => 'yyyy-MM-dd',
])
->add('send', SubmitType::class)
;
}
}

57
src/Service/Endpoint/WeatherHistory.php

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Service\Endpoint;
use DateTime;
use GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class WeatherHistory implements WeatherHistoryInterface
{
private Client $client;
private string $weatherHistoryServer;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->client = new Client();
$this->weatherHistoryServer = $parameterBag->get('weather_history_server');
}
public function getByDate(DateTime $date): array
{
$url = sprintf('%s/json-rpc', $this->weatherHistoryServer);
$response = $this->client->request('POST', $url, [
'headers' => [
'Accept-Encoding' => 'gzip',
'Content-Type' => 'text/plain',
],
'body' => sprintf(
'{"jsonrpc": "2.0", "method": "weather.getByDate", "params": {"date": "%s"}, "id": 1}',
$date->format('Y-m-d')
),
]);
return json_decode($response->getBody()->getContents(), true);
}
public function getHistory(int $numberOfDays = 30): array
{
$url = sprintf('%s/json-rpc', $this->weatherHistoryServer);
$response = $this->client->request('POST', $url, [
'headers' => [
'Accept-Encoding' => 'gzip',
'Content-Type' => 'text/plain',
],
'body' => sprintf(
'{"jsonrpc": "2.0", "method": "weather.getHistory", "params": {"lastDays": %d}, "id": 1}',
$numberOfDays
),
]);
return json_decode($response->getBody()->getContents(), true);
}
}

14
src/Service/Endpoint/WeatherHistoryInterface.php

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Service\Endpoint;
use DateTime;
interface WeatherHistoryInterface
{
public function getByDate(DateTime $date): array;
public function getHistory(int $numberOfDays = 30): array;
}

59
symfony.lock

@ -110,6 +110,9 @@
"localheinz/diff": {
"version": "1.1.1"
},
"monolog/monolog": {
"version": "2.2.0"
},
"myclabs/deep-copy": {
"version": "1.10.2"
},
@ -310,6 +313,9 @@
".env"
]
},
"symfony/form": {
"version": "v5.2.2"
},
"symfony/framework-bundle": {
"version": "5.2",
"recipe": {
@ -339,12 +345,36 @@
"symfony/http-kernel": {
"version": "v5.2.2"
},
"symfony/intl": {
"version": "v5.2.2"
},
"symfony/monolog-bridge": {
"version": "v5.2.2"
},
"symfony/monolog-bundle": {
"version": "3.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "3.3",
"ref": "d7249f7d560f6736115eee1851d02a65826f0a56"
},
"files": [
"config/packages/dev/monolog.yaml",
"config/packages/prod/deprecations.yaml",
"config/packages/prod/monolog.yaml",
"config/packages/test/monolog.yaml"
]
},
"symfony/options-resolver": {
"version": "v5.2.2"
},
"symfony/polyfill-intl-grapheme": {
"version": "v1.22.0"
},
"symfony/polyfill-intl-icu": {
"version": "v1.22.0"
},
"symfony/polyfill-intl-normalizer": {
"version": "v1.22.0"
},
@ -363,6 +393,12 @@
"symfony/process": {
"version": "v5.2.2"
},
"symfony/property-access": {
"version": "v5.2.2"
},
"symfony/property-info": {
"version": "v5.2.2"
},
"symfony/routing": {
"version": "5.1",
"recipe": {
@ -386,6 +422,26 @@
"symfony/string": {
"version": "v5.2.2"
},
"symfony/translation-contracts": {
"version": "v2.3.0"
},
"symfony/twig-bridge": {
"version": "v5.2.2"
},
"symfony/twig-bundle": {
"version": "5.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "5.0",
"ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d"
},
"files": [
"config/packages/test/twig.yaml",
"config/packages/twig.yaml",
"templates/base.html.twig"
]
},
"symfony/var-dumper": {
"version": "v5.2.2"
},
@ -398,6 +454,9 @@
"theseer/tokenizer": {
"version": "1.2.0"
},
"twig/twig": {
"version": "v3.2.1"
},
"webmozart/assert": {
"version": "1.9.1"
}

66
templates/base.html.twig

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
{% block javascripts %}{% endblock %}
</head>
<body>
{% block body %}
{% if weatherForm is defined and weatherForm %}
<h3>Погода за день</h3>
{{ form_errors(weatherForm) }}
{{ form_start(weatherForm) }}
{{ form_widget(weatherForm) }}
{{ form_end(weatherForm) }}
{% if weather.error.code is defined and weather.error.code
and weather.error.message is defined and weather.error.message
%}
<p> {{ weather.error.code }} {{ weather.error.message }}.</p>
{% else %}
{% if weather.result.temp is defined and weather.result.temp %}
<p>Температура {{ weather.result.temp }} градусов</p>
{% else %}
<p>Температура за указанный день не указана</p>
{% endif %}
{% endif %}
{% endif %}
{% if history is defined and history %}
<h3>Погода за последние 30 дней</h3>
{% if history.error.code is defined and history.error.code
and history.error.message is defined and history.error.message
%}
<p>{{ history.error.code }} {{ history.error.message }}</p>
{% else %}
{% if history.result is defined and history.result %}
<table border="1">
<tr>
<th>Дата</th>
<th>Температура</th>
</tr>
{% for element in history.result %}
<tr>
<td>
{% if element.date_at is defined and element.date_at %}
{{ element.date_at }}
{% endif %}
</td>
<td>
{% if element.temp is defined and element.temp %}
{{ element.temp }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endif %}
{% endif %}
{% endblock %}
</body>
</html>

10
tests/_support/ApiTester.php

@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests;
class ApiTester extends \Codeception\Actor
{
use _generated\ApiTesterActions;
}

9
tests/_support/Helper/Api.php

@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\Helper;
class Api extends \Codeception\Module
{
}

2
tests/acceptance.suite.yml

@ -6,7 +6,7 @@ modules:
config:
PhpBrowser:
url: http://nginx/
url: http://weather_site_nginx_1/
proxy: ''
curl:
CURLOPT_TIMEOUT: 10

60
tests/acceptance/Controller/IndexControllerCest.php

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Tests\acceptance\Controller;
use App\Tests\AcceptanceTester;
use Codeception\Example;
use DateTime;
class IndexControllerCest
{
private AcceptanceTester $tester;
public function _before(AcceptanceTester $tester): void
{
$this->tester = $tester;
}
public function testBaseAvailability(): void
{
$this->tester->amOnPage('/');
$this->tester->seeResponseCodeIs(200);
}
/**
* @dataProvider dataDateForm
*/
public function testDateForm(Example $example): void
{
$this->tester->amOnPage('/');
$this->tester->fillField("//input[@id='weather_form_date']", $example['fillField']);
$this->tester->click('Send');
$this->tester->seeCurrentUrlEquals('/');
$this->tester->seeResponseCodeIs(200);
$this->tester->see($example['see']);
}
protected function dataDateForm(): array
{
return [
[
'fillField' => (new DateTime())->format('Y-m-d'),
'see' => 'градусов',
],
[
'fillField' => '9999-01-01',
'see' => 'Температура за указанный день не указана',
],
];
}
public function testWeatherTable(): void
{
$this->tester->amOnPage('/');
$this->tester->see('Погода за последние 30 дней');
$this->tester->see('Дата', '//th');
$this->tester->see('Температура', '//th');
}
}

23
tests/acceptance/Controller/PageControllerCest.php

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\acceptance\Controller;
use App\Tests\AcceptanceTester;
class PageControllerCest
{
private AcceptanceTester $tester;
public function _before(AcceptanceTester $tester): void
{
$this->tester = $tester;
}
public function testBaseAvailability(): void
{
$this->tester->amOnPage('/');
$this->tester->seeResponseCodeIs(400);
}
}

8
tests/api.suite.yml

@ -1,8 +0,0 @@
actor: ApiTester
modules:
enabled:
- REST:
url: http://nginx/
depends: PhpBrowser
part: Json
- \App\Tests\Helper\Api

36
tests/api/Controller/PageControllerCest.php

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\api\Controller;
use App\Tests\ApiTester;
use Codeception\Example;
use Iterator;
class PageControllerCest
{
private ApiTester $tester;
public function _before(ApiTester $tester): void
{
$this->tester = $tester;
}
/**
* @dataProvider dataTestBaseAvailability
*/
public function testBaseAvailability(Example $example): void
{
$this->tester->sendGet($example['url']);
$this->tester->seeResponseCodeIs(400);
$this->tester->seeResponseIsJson();
}
protected static function dataTestBaseAvailability(): Iterator
{
yield [
'url' => '/',
];
}
}