заготовка для проекта

Есть listener обрабатывающий запрос.
Валидирующий данные
трансформирующие его в дто
Добавил тесты
и конфигурации для докера
Добавил зависимости
master
Rinsvent 3 years ago
commit 36d4a935b2
  1. 7
      .gitignore
  2. 18
      .gitlab-ci.yml
  3. 33
      Makefile
  4. 4
      Readme.md
  5. 9
      bin/docker/ci.sh
  6. 8
      bin/docker/prepare-ci.sh
  7. 16
      codeception.yml
  8. 34
      composer.json
  9. 5718
      composer.lock
  10. 15
      docker-compose-ci.yml
  11. 16
      docker-compose.yml
  12. 11
      src/Annotation/HeaderKey.php
  13. 12
      src/Annotation/RequestDTO.php
  14. 19
      src/DTO/Error.php
  15. 29
      src/DTO/ErrorCollection.php
  16. 17
      src/DependencyInjection/RequestExtension.php
  17. 109
      src/EventListener/RequestListener.php
  18. 10
      src/RequestBundle.php
  19. 5
      src/Resources/config/services.yaml
  20. 0
      tests/_data/.gitkeep
  21. 2
      tests/_output/.gitignore
  22. 0
      tests/_support/.gitkeep
  23. 24
      tests/_support/Helper/Unit.php
  24. 27
      tests/_support/UnitTester.php
  25. 2
      tests/_support/_generated/.gitignore
  26. 10
      tests/unit.suite.yml
  27. 69
      tests/unit/Listener/FillTest.php
  28. 18
      tests/unit/Listener/fixtures/FillTest/Controller.php
  29. 14
      tests/unit/Listener/fixtures/FillTest/HelloRequest.php

7
.gitignore vendored

@ -0,0 +1,7 @@
.idea
/var/
/vendor/
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###

@ -0,0 +1,18 @@
image: dh.rinsvent.ru/ci
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
services:
- docker:dind
before_script:
- bash bin/docker/prepare-ci.sh
build:
stage: build
script:
- docker login --username ${REGISTRY_USERNAME} --password ${REGISTRY_PASSWORD} dh.rinsvent.ru
- bash bin/docker/ci.sh

@ -0,0 +1,33 @@
auth:
docker exec -it -u1000:1000 requestbundle_php bash
auth-root:
docker exec -it requestbundle_php bash
test:
bin/codecept run $p
coverage:
bin/codecept run --coverage --coverage-html=/app/var/temp.html
# make out container
coverage-open:
google-chrome var/temp.html/index.html
#docker
start:
docker-compose up -d
stop:
docker-compose down
pull:
docker-compose pull
restart: stop start
restart-php:
docker-compose restart backend-php-fpm
down-clear:
docker-compose down -v --remove-orphans
init: down-clear pull start
#prepare
prepare-environment:
bash bin/docker/prepare.sh

@ -0,0 +1,4 @@
Request bundle
=

@ -0,0 +1,9 @@
#!/bin/bash
docker-compose -f ./docker-compose-ci.yml up -d
echo 'composer installing'
docker exec -i apisdkgenerator_php composer install -q
echo 'composer installed !!'
docker exec -i apisdkgenerator_php vendor/bin/codecept run --coverage

@ -0,0 +1,8 @@
#!/bin/bash
FULL_PROJECT_NETWORK=$(docker network ls | grep full-project)
if [ -z "$FULL_PROJECT_NETWORK" ]
then
docker network create full-project --subnet=192.168.221.0/25
fi

@ -0,0 +1,16 @@
namespace: Rinsvent\RequestBundle\Tests
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
coverage:
enabled: true
include:
- src/*

@ -0,0 +1,34 @@
{
"name": "rinsvent/request-bundle",
"description": "Request bundle",
"type": "project",
"license": "proprietary",
"require": {
"php": "^8.0",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-json": "*",
"jms/serializer": "^3.13",
"symfony/validator": "^5.3",
"symfony/cache": "^5.3"
},
"require-dev": {
"codeception/codeception": "^4.1",
"codeception/module-phpbrowser": "^1.0.0",
"codeception/module-asserts": "^1.0.0",
"symfony/http-kernel": "^5.3",
"symfony/http-foundation": "^5.3",
"symfony/routing": "^5.3"
},
"autoload": {
"psr-4": {
"tests\\": "tests/",
"Rinsvent\\RequestBundle\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Rinsvent\\RequestBundle\\Tests\\": "tests/"
}
}
}

5718
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
version: '3.3'
services:
requestbundle_php:
image: dh.rinsvent.ru/php8dev
container_name: requestbundle_php
volumes:
- ./:/app
environment:
USE_COMPOSER_SCRIPTS: 0
networks:
default:
external:
name: full-project

@ -0,0 +1,16 @@
version: '3.3'
services:
requestbundle_php:
image: dh.rinsvent.ru/php8dev
container_name: requestbundle_php
volumes:
- ./:/app
environment:
USE_COMPOSER_SCRIPTS: 1
PHP_IDE_CONFIG: "serverName=requestbundle_php"
networks:
default:
external:
name: full-project

@ -0,0 +1,11 @@
<?php
namespace Rinsvent\RequestBundle\Annotation;
#[\Attribute]
class HeaderKey
{
public function __construct(
public string $key
) {}
}

@ -0,0 +1,12 @@
<?php
namespace Rinsvent\RequestBundle\Annotation;
#[\Attribute]
class RequestDTO
{
public function __construct(
public string $className,
public string $jsonPath = '$',
) {}
}

@ -0,0 +1,19 @@
<?php
namespace Rinsvent\RequestBundle\DTO;
class Error
{
public function __construct(
public string $message,
public string $path,
) {}
public function format(): array
{
return [
'message' => $this->message,
'path' => $this->path,
];
}
}

@ -0,0 +1,29 @@
<?php
namespace Rinsvent\RequestBundle\DTO;
class ErrorCollection
{
/** @var Error[] $items */
public array $items = [];
public function add(Error $error): self
{
$this->items[] = $error;
return $this;
}
public function hasErrors(): bool
{
return count($this->items);
}
public function format(): array
{
$result = [];
foreach ($this->items as $item) {
$result[] = $item->format();
}
return $result;
}
}

@ -0,0 +1,17 @@
<?php
namespace Rinsvent\RequestBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class RequestExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');
}
}

@ -0,0 +1,109 @@
<?php
namespace Rinsvent\RequestBundle\EventListener;
use JMS\Serializer\SerializerBuilder;
use ReflectionMethod;
use ReflectionObject;
use Rinsvent\RequestBundle\Annotation\RequestDTO;
use Rinsvent\RequestBundle\Annotation\HeaderKey;
use Rinsvent\RequestBundle\DTO\Error;
use Rinsvent\RequestBundle\DTO\ErrorCollection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Validator\Validation;
// todo базовая заготовка. Требуется рефакторинг
class RequestListener
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
$controller = $request->get('_controller');
if (is_string($controller)) {
$controller = explode('::', $controller);
$method = new ReflectionMethod($controller[0], $controller[1]);
}
if (is_callable($controller)){
$method = new ReflectionMethod($controller[0], $controller[1]);
}
if (!isset($method)) {
return;
}
$attributes = $method->getAttributes(RequestDTO::class);
$attribute = $attributes[0] ?? null;
if ($attribute) {
/** @var RequestDTO $requestDTO */
$requestDTO = $attribute->newInstance();
$requestDTOInstance = $this->grabRequestDTO($requestDTO->className, $request->getContent(), $request->query->all(), $request->request->all(), $request->headers->all());
$errorCollection = $this->validate($requestDTOInstance);
if ($errorCollection->hasErrors()) {
$event->setResponse(new JsonResponse(['errors' => $errorCollection->format()], Response::HTTP_BAD_REQUEST));
} else {
$request->attributes->set($requestDTOInstance::class, $requestDTOInstance);
}
}
}
protected function validate(object $data): ErrorCollection
{
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping(true)
->addDefaultDoctrineAnnotationReader()
->getValidator();
$violations = $validator->validate($data);
$errorCollection = new ErrorCollection();
if ($violations->count()) {
foreach ($violations as $violation) {
$errorCollection->add(new Error($violation->getMessage(), $violation->getPropertyPath()));
}
}
return $errorCollection;
}
/**
* todo переделать на перебор филлеров
* Сделать регистрацию филлеров
* и выексти филеры заполнения entity и documents в отдельных бандлах
*/
protected function grabRequestDTO(string $requestClass, string $content, array $queryParameters = [], array $parameters = [], array $headers = [])
{
$serializer = SerializerBuilder::create()->build();
if ($content) {
$object = $serializer->deserialize($content, $requestClass, 'json');
} else {
$object = new $requestClass;
}
$this->fillFromData($object, $queryParameters);
$this->fillFromData($object, $parameters);
$this->fillFromData($object, $headers);
return $object;
}
protected function fillFromData(object $object, array $data): object
{
$reflectionObject = new ReflectionObject($object);
$properties = $reflectionObject->getProperties();
foreach ($properties as $property) {
$attributes = $property->getAttributes(HeaderKey::class);
$attribute = $attributes[0] ?? null;
if ($attribute) {
/** @var HeaderKey $headerKey */
$headerKey = $attribute->newInstance();
$value = $data[strtolower($headerKey->key)][0] ?? null;
} else {
$value = $data[$property->getName()] ?? null;
}
if ($value) {
$property->setValue($object, $value);
}
}
return $object;
}
}

@ -0,0 +1,10 @@
<?php
namespace Rinsvent\RequestBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class RequestBundle extends Bundle
{
}

@ -0,0 +1,5 @@
services:
Rinsvent\RequestBundle\EventListener\RequestListener:
autowire: true
tags:
- { name: kernel.event_listener, event: kernel.request}

@ -0,0 +1,2 @@
*
!.gitignore

@ -0,0 +1,24 @@
<?php
namespace Rinsvent\RequestBundle\Tests\Helper;
// here you can define custom actions
// all public methods declared in helper class will be available in $I
use Rinsvent\ApiSDKGenerator\DTO\Writer\Config;
use Rinsvent\ApiSDKGenerator\Service\Writer;
class Unit extends \Codeception\Module
{
public function getWriter(string $lang = 'php'): Writer
{
return new Writer(
new Config(
dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'templates',
dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'var/tests/cache',
$lang,
dirname(dirname(dirname(__DIR__))) . DIRECTORY_SEPARATOR . 'var/tests/result',
'Rinsvent\\AuthSDK'
)
);
}
}

@ -0,0 +1,27 @@
<?php
namespace Rinsvent\RequestBundle\Tests;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method void pause()
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \Codeception\Actor
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

@ -0,0 +1,2 @@
*
!.gitignore

@ -0,0 +1,10 @@
# Codeception Test Suite Configuration
#
# Suite for unit or integration tests.
actor: UnitTester
modules:
enabled:
- Asserts
- \Rinsvent\RequestBundle\Tests\Helper\Unit
step_decorators: ~

@ -0,0 +1,69 @@
<?php
namespace Rinsvent\RequestBundle\Tests\Listener;
use Rinsvent\RequestBundle\Tests\unit\Listener\fixtures\FillTest\Controller;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Rinsvent\RequestBundle\EventListener\RequestListener;
class GenerateTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
// tests
public function testGenerateEnum()
{
$request = Request::create('/hello/igor', 'GET', [
'surname' => 'Surname'
]);
$response = $this->send($request);
$this->assertEquals(3, $request->get('form'));
}
private function send(Request $request): Response
{
$routes = new RouteCollection();
$controller = new Controller();
$routes->add('hello', new Route('/hello/{name}', [
'_controller' => [$controller, 'hello']
]
));
$matcher = new UrlMatcher($routes, new RequestContext());
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher, new RequestStack()));
$listener = new RequestListener();
$dispatcher->addListener('kernel.request', [$listener, 'onKernelRequest']);
$controllerResolver = new ControllerResolver();
$argumentResolver = new ArgumentResolver();
$kernel = new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver);
$response = $kernel->handle($request);
$response->send();
return $response;
}
}

@ -0,0 +1,18 @@
<?php
namespace Rinsvent\RequestBundle\Tests\unit\Listener\fixtures\FillTest;
use Rinsvent\RequestBundle\Annotation\RequestDTO;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class Controller
{
#[RequestDTO(className: HelloRequest::class)]
public function hello(Request $request)
{
return new Response(
sprintf("Hello %s", $request->get('name'))
);
}
}

@ -0,0 +1,14 @@
<?php
namespace Rinsvent\RequestBundle\Tests\unit\Listener\fixtures\FillTest;
use Symfony\Component\Validator\Constraints as Assert;
class HelloRequest
{
#[Assert\NotBlank]
public string $surname;
public int $age;
#[Assert\Email]
public string $email;
}
Loading…
Cancel
Save