├── cache
└── .gitignore
├── tests
├── cache
│ └── .gitignore
├── Stub
│ ├── Routing
│ │ ├── getHello.php
│ │ ├── postHello.php
│ │ └── getMiddlewareHello.php
│ ├── Console
│ │ ├── hello.php
│ │ └── showError.php
│ └── Middlewares
│ │ └── TestMiddleware.php
├── TestCase.php
└── Bleeding
│ ├── Applications
│ ├── LoggerFactoryTest.php
│ ├── ContainerFactoryTest.php
│ ├── WebApplicationTest.php
│ ├── ConsoleApplicationTest.php
│ └── ErrorHandlerTest.php
│ ├── Console
│ ├── Attributes
│ │ └── CommandTest.php
│ ├── CommandTest.php
│ └── CollectCommandTest.php
│ ├── Http
│ ├── ServerRequestFactoryTest.php
│ ├── Attributes
│ │ └── MiddlewareTest.php
│ ├── Exceptions
│ │ └── InternalServerErrorExceptionTest.php
│ └── Middlewares
│ │ ├── ProcessErrorMiddlewareTest.php
│ │ └── ParseBodyMiddlewareTest.php
│ ├── Routing
│ ├── Attributes
│ │ ├── GetTest.php
│ │ └── PostTest.php
│ ├── RouteTest.php
│ ├── CollectRouteTest.php
│ ├── MiddlewareResolverTest.php
│ ├── InvokeControllerTest.php
│ └── RoutingResolverTest.php
│ ├── Support
│ └── ClockTest.php
│ └── Exceptions
│ └── RuntimeExceptionTest.php
├── .env.example
├── .vscode
└── extensions.json
├── .editorconfig
├── Edge
├── Controllers
│ ├── getHome.php
│ └── Users
│ │ └── postLogin.php
├── Repositories
│ ├── definitions.php
│ ├── InMemoryUserRepository.php
│ └── PDOUserRepository.php
├── Commands
│ └── hello.php
├── User
│ ├── IUserRepository.php
│ ├── LoginService.php
│ └── UserEntity.php
└── Applications
│ ├── ConsoleApplication.php
│ ├── ContainerFactory.php
│ └── WebApplication.php
├── phpcs.xml.dist
├── public
└── index.php
├── bleeding
├── Bleeding
├── Http
│ ├── Exceptions
│ │ ├── BadRequestException.php
│ │ ├── NotFoundException.php
│ │ ├── ForbiddenException.php
│ │ ├── UnauthorizedException.php
│ │ ├── MethodNotAllowedException.php
│ │ └── InternalServerErrorException.php
│ ├── ServerRequestFactoryInterface.php
│ ├── ServerRequestFactory.php
│ ├── definitions.php
│ ├── Attributes
│ │ └── Middleware.php
│ └── Middlewares
│ │ ├── ParseBodyMiddleware.php
│ │ └── ProcessErrorMiddleware.php
├── Applications
│ ├── Application.php
│ ├── LoggerFactory.php
│ ├── ContainerFactory.php
│ ├── ErrorHandler.php
│ ├── WebApplication.php
│ └── ConsoleApplication.php
├── Console
│ ├── Attributes
│ │ └── Command.php
│ ├── Command.php
│ └── CollectCommand.php
├── Support
│ └── Clock.php
├── Routing
│ ├── Attributes
│ │ ├── Get.php
│ │ └── Post.php
│ ├── MiddlewareResolver.php
│ ├── Route.php
│ ├── InvokeController.php
│ ├── RoutingResolver.php
│ └── CollectRoute.php
└── Exceptions
│ └── RuntimeException.php
├── psalm.xml
├── Dockerfile
├── phpunit.xml
├── container
├── nginx
│ └── nginx.conf
└── web
│ └── www.conf
├── .gitignore
├── README.md
├── docker-compose.yml
├── composer.json
└── LICENSE
/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/tests/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_PATH=.
2 | # outputs debug log
3 | DEBUG_MODE=true
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "editorconfig.editorconfig"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/Edge/Controllers/getHome.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Routing\Attributes\Get;
11 |
12 | return
13 | #[Get('/')]
14 | fn () => ['Hello' => 'world'];
15 |
--------------------------------------------------------------------------------
/tests/Stub/Routing/getHello.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Routing\Attributes\Get;
11 |
12 | return
13 | #[Get('/')]
14 | fn () => ['Hello' => 'world'];
15 |
--------------------------------------------------------------------------------
/tests/Stub/Routing/postHello.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Routing\Attributes\Post;
11 |
12 | return
13 | #[Post('/')]
14 | fn () => ['Hello' => 'world'];
15 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | The coding standard for Bleeding.
4 |
5 | Bleeding
6 | Edge
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Edge/Repositories/definitions.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Repositories;
11 |
12 | use Edge\User\IUserRepository;
13 |
14 | use function DI\get;
15 |
16 | return [
17 | IUserRepository::class => get(InMemoryUserRepository::class),
18 | ];
19 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | define('ENTRY_TIME', microtime(true));
11 |
12 | // autoload
13 | require implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'vendor', 'autoload.php']);
14 |
15 | $exitCode = (new Edge\Applications\WebApplication)->run();
16 |
17 | exit($exitCode);
18 |
--------------------------------------------------------------------------------
/tests/Stub/Routing/getMiddlewareHello.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Http\Attributes\Middleware;
11 | use Bleeding\Routing\Attributes\Get;
12 |
13 | return
14 | #[Get('/middleware')]
15 | #[Middleware(\Tests\Stub\Middlewares\TestMiddleware::class)]
16 | fn () => ['Hello' => 'world'];
17 |
--------------------------------------------------------------------------------
/bleeding:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
6 | * @copyright 2020- Masaru Yamagishi
7 | */
8 |
9 | declare(strict_types=1);
10 |
11 | define('ENTRY_TIME', microtime(true));
12 |
13 | // autoload
14 | require implode(DIRECTORY_SEPARATOR, [__DIR__, 'vendor', 'autoload.php']);
15 |
16 | $exitCode = (new Edge\Applications\ConsoleApplication)->run();
17 |
18 | exit($exitCode);
19 |
--------------------------------------------------------------------------------
/Edge/Commands/hello.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Console\Attributes\Command;
11 | use Symfony\Component\Console\Input\InputInterface;
12 | use Symfony\Component\Console\Output\OutputInterface;
13 |
14 | return
15 | #[Command('hello')]
16 | fn (InputInterface $input, OutputInterface $output) => $output->writeln('Hello world');
17 |
--------------------------------------------------------------------------------
/tests/Stub/Console/hello.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Console\Attributes\Command;
11 | use Symfony\Component\Console\Input\InputInterface;
12 | use Symfony\Component\Console\Output\OutputInterface;
13 |
14 | return
15 | #[Command('hello')]
16 | fn (InputInterface $input, OutputInterface $output) => $output->writeln('Hello world');
17 |
--------------------------------------------------------------------------------
/tests/Stub/Console/showError.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Console\Attributes\Command;
11 | use Symfony\Component\Console\Input\InputInterface;
12 | use Symfony\Component\Console\Output\OutputInterface;
13 |
14 | return
15 | #[Command('show-error')]
16 | fn (InputInterface $input, OutputInterface $output) => throw new \RuntimeException('Show error', 2);
17 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/BadRequestException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | /**
13 | * 400 Client HTTP Exception
14 | * @package Bleeding\Http\Exceptions
15 | */
16 | class BadRequestException extends InternalServerErrorException
17 | {
18 | /** {@inheritdoc} */
19 | protected const MESSAGE = 'Bad Request';
20 |
21 | /** {@inheritdoc} */
22 | protected const CODE = 400;
23 | }
24 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/NotFoundException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | /**
13 | * 404 NotFound HTTP Exception
14 | * @package Bleeding\Http\Exceptions
15 | */
16 | final class NotFoundException extends BadRequestException
17 | {
18 | /** {@inheritdoc} */
19 | protected const MESSAGE = 'Not Found';
20 |
21 | /** @var int Exception code */
22 | protected const CODE = 404;
23 | }
24 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/ForbiddenException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | /**
13 | * 404 NotFound HTTP Exception
14 | * @package Bleeding\Http\Exceptions
15 | */
16 | final class ForbiddenException extends BadRequestException
17 | {
18 | /** {@inheritdoc} */
19 | protected const MESSAGE = 'Forbidden';
20 |
21 | /** @var int Exception code */
22 | protected const CODE = 403;
23 | }
24 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/UnauthorizedException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | /**
13 | * 404 NotFound HTTP Exception
14 | * @package Bleeding\Http\Exceptions
15 | */
16 | final class UnauthorizedException extends BadRequestException
17 | {
18 | /** {@inheritdoc} */
19 | protected const MESSAGE = 'Unauthorized';
20 |
21 | /** @var int Exception code */
22 | protected const CODE = 401;
23 | }
24 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/MethodNotAllowedException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | /**
13 | * 405 Method not allowed HTTP Exception
14 | * @package Bleeding\Http\Exceptions
15 | */
16 | final class MethodNotAllowedException extends BadRequestException
17 | {
18 | /** {@inheritdoc} */
19 | protected const MESSAGE = 'Method Not Allowed';
20 |
21 | /** @var int Exception code */
22 | protected const CODE = 405;
23 | }
24 |
--------------------------------------------------------------------------------
/Edge/User/IUserRepository.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\User;
11 |
12 | /**
13 | * @package Edge\User
14 | */
15 | interface IUserRepository
16 | {
17 | /**
18 | * @param int $id
19 | * @return ?UserEntity
20 | */
21 | public function findById(int $id): ?UserEntity;
22 |
23 | /**
24 | * @param string $username
25 | * @return ?UserEntity
26 | */
27 | public function findByName(string $username): ?UserEntity;
28 | }
29 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Bleeding/Http/ServerRequestFactoryInterface.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http;
11 |
12 | use Psr\Http\Message\ServerRequestInterface;
13 |
14 | /**
15 | * ServerRequest createFromGlobals
16 | * @package Bleeding\Http
17 | */
18 | interface ServerRequestFactoryInterface
19 | {
20 | /**
21 | * Create PSR-7 ServerRequest from globals
22 | *
23 | * @return ServerRequestInterface
24 | */
25 | public function createFromGlobals(): ServerRequestInterface;
26 | }
27 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests;
11 |
12 | use Bleeding\ContainerFactory;
13 | use DI\Container;
14 | use PHPUnit\Framework\TestCase as TestCaseBase;
15 |
16 | /**
17 | * TestCase
18 | * @package Tests
19 | */
20 | abstract class TestCase extends TestCaseBase
21 | {
22 | /**
23 | * Create Bleeding container
24 | *
25 | * @return Container
26 | */
27 | protected function createContainer(): Container
28 | {
29 | return ContainerFactory::create();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Bleeding/Applications/Application.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | /**
13 | * Main application interface
14 | * @package Bleeding\Applications
15 | */
16 | interface Application
17 | {
18 | /** @var string APP_NAME Application Name */
19 | public const APP_NAME = 'Bleeding';
20 |
21 | /** @var string APP_VERSION Application Version */
22 | public const APP_VERSION = '1.0.0';
23 |
24 | /**
25 | * Run application
26 | *
27 | * @return int exitCode
28 | */
29 | public function run(): int;
30 | }
31 |
--------------------------------------------------------------------------------
/Bleeding/Http/ServerRequestFactory.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http;
11 |
12 | use Laminas\Diactoros\ServerRequestFactory as DiactorosServerRequestFactory;
13 | use Psr\Http\Message\ServerRequestInterface;
14 |
15 | /**
16 | * @package Bleeding\Http
17 | */
18 | final class ServerRequestFactory implements ServerRequestFactoryInterface
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function createFromGlobals(): ServerRequestInterface
24 | {
25 | return DiactorosServerRequestFactory::fromGlobals();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Bleeding/Http/definitions.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2020- Masaru Yamagishi
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace Bleeding\Http;
13 |
14 | use Laminas\Diactoros\ResponseFactory;
15 | use Laminas\Diactoros\StreamFactory;
16 | use Psr\Http\Message\ResponseFactoryInterface;
17 | use Psr\Http\Message\StreamFactoryInterface;
18 |
19 | use function DI\get;
20 |
21 | return [
22 | ServerRequestFactoryInterface::class => get(ServerRequestFactory::class),
23 | ResponseFactoryInterface::class => get(ResponseFactory::class),
24 | StreamFactoryInterface::class => get(StreamFactory::class),
25 | ];
26 |
--------------------------------------------------------------------------------
/tests/Stub/Middlewares/TestMiddleware.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Stub\Middlewares;
11 |
12 | use Psr\Http\Message\ServerRequestInterface;
13 | use Psr\Http\Message\ResponseInterface;
14 | use Psr\Http\Server\MiddlewareInterface;
15 | use Psr\Http\Server\RequestHandlerInterface;
16 |
17 | /**
18 | * Middleware for test
19 | */
20 | class TestMiddleware implements MiddlewareInterface
21 | {
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
26 | {
27 | return $handler->handle($request);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Bleeding/Applications/LoggerFactoryTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Applications;
11 |
12 | use Bleeding\Applications\LoggerFactory;
13 | use Psr\Log\LoggerInterface;
14 | use Tests\TestCase;
15 |
16 | /**
17 | * @package Tests\Bleeding\Applications
18 | * @coversDefaultClass \Bleeding\Applications\LoggerFactory
19 | * @immutable
20 | */
21 | final class LoggerFactoryTest extends TestCase
22 | {
23 | /**
24 | * @test
25 | * @covers ::create
26 | */
27 | public function testCreate(): void
28 | {
29 | $logger = LoggerFactory::create('Bleeding Test');
30 |
31 | $this->assertInstanceOf(LoggerInterface::class, $logger);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Edge/Applications/ConsoleApplication.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Applications;
11 |
12 | use Bleeding\Applications\ConsoleApplication as ConsoleApplicationBase;
13 | use DI\Container;
14 |
15 | /**
16 | * @package Edge\Applications
17 | */
18 | class ConsoleApplication extends ConsoleApplicationBase
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function createContainer(): Container
24 | {
25 | return ContainerFactory::create();
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | protected function getCommandDirectory(): string
32 | {
33 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Commands']);
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/Bleeding/Console/Attributes/Command.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2020- Masaru Yamagishi
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace Bleeding\Console\Attributes;
13 |
14 | use Attribute;
15 |
16 | /**
17 | * Function that processes console command
18 | * @package Bleeding\Console\Attributes
19 | */
20 | #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
21 | class Command
22 | {
23 | /**
24 | * Command
25 | *
26 | * @param string $definition
27 | */
28 | public function __construct(private string $definition)
29 | {}
30 |
31 | /**
32 | * Get command definition
33 | * @return string
34 | */
35 | public function getDefinition(): string
36 | {
37 | return $this->definition;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Bleeding/Console/Command.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Console;
11 |
12 | /**
13 | * Command
14 | *
15 | * @package Bleeding\Console
16 | */
17 | final class Command
18 | {
19 | /**
20 | * @param string $path
21 | * @param callable $func
22 | */
23 | public function __construct(
24 | private string $definition,
25 | private $func
26 | ) {}
27 |
28 | /**
29 | * get command definition
30 | * @return string
31 | */
32 | public function getDefinition(): string
33 | {
34 | return $this->definition;
35 | }
36 |
37 | /**
38 | * get controller function
39 | * @return callable
40 | */
41 | public function getFunc(): callable
42 | {
43 | return $this->func;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Bleeding/Console/Attributes/CommandTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Console\Attributes;
11 |
12 | use Bleeding\Console\Attributes\Command;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Console\Attributes
17 | * @coversDefaultClass \Bleeding\Console\Attributes\Command
18 | * @immutable
19 | */
20 | final class CommandTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | * @covers ::__construct
25 | * @covers ::getDefinition
26 | */
27 | public function testConstruct()
28 | {
29 | $command = new Command(
30 | $definition = 'hello'
31 | );
32 |
33 | $this->assertInstanceOf(Command::class, $command);
34 | $this->assertSame($definition, $command->getDefinition());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Bleeding/Support/Clock.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Support;
11 |
12 | use Cake\Chronos\Chronos;
13 | use Cake\Chronos\ChronosInterface;
14 |
15 | use const ENTRY_TIME;
16 |
17 | /**
18 | * @package Bleeding\Support
19 | */
20 | final class Clock
21 | {
22 | /**
23 | * Respect ENTRY_TIME defined by entrypoint
24 | *
25 | * @return ChronosInterface
26 | */
27 | public static function entry(): ChronosInterface
28 | {
29 | $timestamp = defined('ENTRY_TIME') ? ENTRY_TIME : time();
30 |
31 | return new Chronos('@' . intval($timestamp));
32 | }
33 |
34 | /**
35 | * Get now
36 | *
37 | * @return ChronosInterface
38 | */
39 | public static function now(): ChronosInterface
40 | {
41 | return Chronos::now();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Bleeding/Applications/LoggerFactory.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | use Monolog\Formatter\JsonFormatter;
13 | use Monolog\Formatter\LineFormatter;
14 | use Monolog\Handler\StreamHandler;
15 | use Monolog\Logger;
16 | use Symfony\Component\Console\Logger\ConsoleLogger;
17 | use Symfony\Component\Console\Output\OutputInterface;
18 |
19 | /**
20 | * @package Bleeding\Applications
21 | */
22 | class LoggerFactory
23 | {
24 | /**
25 | * Create Logger for Web
26 | * @return Logger
27 | */
28 | public static function create(string $name = 'Bleeding'): Logger
29 | {
30 | $handler = new StreamHandler('php://stdout');
31 | $handler->setFormatter(new JsonFormatter());
32 |
33 | $logger = new Logger($name);
34 | $logger->pushHandler($handler);
35 |
36 | return $logger;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/Bleeding/Console/CommandTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Console;
11 |
12 | use Bleeding\Console\Command;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Console
17 | * @coversDefaultClass \Bleeding\Console\Command
18 | * @immutable
19 | */
20 | final class CommandTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | * @covers ::__construct
25 | * @covers ::getDefinition
26 | * @covers ::getFunc
27 | */
28 | public function testConstruct()
29 | {
30 | $command = new Command(
31 | $definition = 'hello',
32 | $func = fn () => true
33 | );
34 |
35 | $this->assertInstanceOf(Command::class, $command);
36 | $this->assertSame($definition, $command->getDefinition());
37 | $this->assertSame($func, $command->getFunc());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Bleeding/Http/ServerRequestFactoryTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Http;
11 |
12 | use Bleeding\Http\ServerRequestFactory;
13 | use Bleeding\Http\ServerRequestFactoryInterface;
14 | use Psr\Http\Message\ServerRequestInterface;
15 | use Tests\TestCase;
16 |
17 | /**
18 | * @package Tests\Bleeding\Http
19 | * @coversDefaultClass \Bleeding\Http\ServerRequestFactory
20 | */
21 | final class ServerRequestFactoryTest extends TestCase
22 | {
23 | /**
24 | * @test
25 | * @covers ::createFromGlobals
26 | */
27 | public function testCreateFromGlobals(): void
28 | {
29 | $factory = new ServerRequestFactory();
30 | $serverRequest = $factory->createFromGlobals();
31 |
32 | $this->assertInstanceOf(ServerRequestFactoryInterface::class, $factory);
33 | $this->assertInstanceOf(ServerRequestInterface::class, $serverRequest);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.0-rc-fpm-alpine
2 |
3 | LABEL maintainer="github.com/il-m-yamagishi" \
4 | org.label-schema.docker.dockerfile="/Dockerfile" \
5 | org.label-schema.name="Bleeding Edge PHP 8 Framework" \
6 | org.label-schema.url="https://github.com/il-m-yamagishi/bleeding" \
7 | org.label-schema.vcs-url="https://github.com/il-m-yamagishi/bleeding"
8 |
9 | ENV PORT 8080
10 |
11 | # install extension
12 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/
13 |
14 | RUN install-php-extensions gettext opcache pdo_mysql redis zip
15 |
16 | # composer
17 |
18 | ENV COMPOSER_ALLOW_SUPERUSER 1
19 | ENV COMPOSER_HOME /tmp
20 | ENV COMPOSER_CACHE_DIR /tmp
21 |
22 | COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
23 |
24 | RUN set -eux; \
25 | apk add --no-cache bash git make unzip zip \
26 | && mkdir -p /var/run/php-fpm \
27 | && chmod 777 /var/run/php-fpm
28 |
29 | VOLUME ["/var/run/php-fpm"]
30 | VOLUME ["/usr/src/bleeding"]
31 |
32 | COPY . /usr/src/bleeding
33 | WORKDIR /usr/src/bleeding/public
34 |
--------------------------------------------------------------------------------
/Bleeding/Http/Attributes/Middleware.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2020- Masaru Yamagishi
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace Bleeding\Http\Attributes;
13 |
14 | use Attribute;
15 |
16 | /**
17 | * Path specific middleware
18 | * @package Bleeding\Http\Attributes
19 | */
20 | #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
21 | class Middleware
22 | {
23 | /** @var string[] */
24 | private array $middlewareNames = [];
25 |
26 | /**
27 | * Construct Middleware Information
28 | *
29 | * @param string|string[] $middlewares
30 | */
31 | public function __construct($middlewares)
32 | {
33 | $this->middlewareNames = (array)$middlewares;
34 | }
35 |
36 | /**
37 | * Get middleware names
38 | *
39 | * @return string[]
40 | */
41 | public function getMiddlewareNames(): array
42 | {
43 | return $this->middlewareNames;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Edge/Repositories/InMemoryUserRepository.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Repositories;
11 |
12 | use Edge\User\IUserRepository;
13 | use Edge\User\UserEntity;
14 |
15 | /**
16 | * @package Edge\Repositories
17 | */
18 | final class InMemoryUserRepository implements IUserRepository
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function findById(int $id): ?UserEntity
24 | {
25 | if ($id === 1) {
26 | return new UserEntity($id, 'test', '$2y$10$R0oLxUu4tenpPWdeGyYELeEoO5SOTMSY7sNQ723aYXVd0uC.l4SEe');
27 | }
28 | return null;
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function findByName(string $username): ?UserEntity
35 | {
36 | if ($username === 'test') {
37 | return new UserEntity(1, $username, '$2y$10$R0oLxUu4tenpPWdeGyYELeEoO5SOTMSY7sNQ723aYXVd0uC.l4SEe');
38 | }
39 | return null;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Edge/Applications/ContainerFactory.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Applications;
11 |
12 | use Bleeding\Applications\ContainerFactory as ContainerFactoryBase;
13 | use DI\ContainerBuilder;
14 |
15 | /**
16 | * @package Edge\Applications
17 | */
18 | final class ContainerFactory extends ContainerFactoryBase
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | protected static function addDefinitions(ContainerBuilder $builder): void
24 | {
25 | parent::addDefinitions($builder);
26 |
27 | $builder->addDefinitions(static::resolveDefinitionsPath('Edge', 'Repositories'));
28 | }
29 |
30 | /**
31 | * resolve definitions path
32 | *
33 | * @param string[] $args
34 | * @return string
35 | */
36 | private static function resolveDefinitionsPath(string ...$args): string
37 | {
38 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', ...$args, 'definitions.php']);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Bleeding/Http/Attributes/MiddlewareTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Http\Attributes;
11 |
12 | use Bleeding\Http\Attributes\Middleware;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Http\Attributes
17 | * @coversDefaultClass \Bleeding\Http\Attributes\Middleware
18 | */
19 | final class MiddlewareTest extends TestCase
20 | {
21 | /**
22 | * @test
23 | * @covers ::__construct
24 | * @covers ::getMiddlewareNames
25 | */
26 | public function testConstruct(): void
27 | {
28 | $attr = new Middleware([]);
29 |
30 | $this->assertInstanceOf(Middleware::class, $attr);
31 | $this->assertSame([], $attr->getMiddlewareNames());
32 |
33 | $attr = new Middleware('foo');
34 |
35 | $this->assertSame(['foo'], $attr->getMiddlewareNames());
36 |
37 | $attr = new Middleware(['foo', 'bar']);
38 |
39 | $this->assertSame(['foo', 'bar'], $attr->getMiddlewareNames());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/Bleeding/Console/CollectCommandTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Console;
11 |
12 | use Bleeding\Console\CollectCommand;
13 | use Tests\TestCase;
14 |
15 | use function implode;
16 |
17 | use const DIRECTORY_SEPARATOR;
18 |
19 | /**
20 | * @package Tests\Bleeding\Console
21 | * @coversDefaultClass \Bleeding\Console\CollectCommand
22 | * @immutable
23 | */
24 | final class CollectCommandTest extends TestCase
25 | {
26 | /**
27 | * @test
28 | * @covers ::collect
29 | * @covers ::checkFile
30 | * @uses \Bleeding\Console\Attributes\Command
31 | * @uses \Bleeding\Console\Command
32 | */
33 | public function testCollect()
34 | {
35 | $baseDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Stub', 'Console']);
36 | $commands = CollectCommand::collect($baseDir);
37 |
38 | $this->assertIsArray($commands);
39 | $this->assertCount(2, $commands);
40 | $this->assertSame('hello', $commands[0]->getDefinition());
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | tests
17 |
18 |
19 |
20 |
22 |
23 | Bleeding
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Edge/User/LoginService.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\User;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 |
14 | use function password_verify;
15 |
16 | /**
17 | * @package Edge\User
18 | */
19 | final class LoginService
20 | {
21 | /**
22 | * @param IUserRepository $repo
23 | */
24 | public function __construct(
25 | private IUserRepository $repo
26 | ) {}
27 |
28 | /**
29 | * @param string $username
30 | * @param string $rawPassword
31 | * @return UserEntity
32 | * @throws RuntimeException
33 | */
34 | public function __invoke(string $username, string $rawPassword): UserEntity
35 | {
36 | $user = $this->repo->findByName($username);
37 |
38 | if (is_null($user)) {
39 | throw RuntimeException::create('ユーザ名またはパスワードが間違っています', 400);
40 | }
41 |
42 | if (!password_verify($rawPassword, $user->getHashedPassword())) {
43 | throw RuntimeException::create('ユーザ名またはパスワードが間違っています', 400);
44 | }
45 |
46 | return $user;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Bleeding/Routing/Attributes/Get.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2020- Masaru Yamagishi
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace Bleeding\Routing\Attributes;
13 |
14 | use Attribute;
15 |
16 | use function trim;
17 |
18 | /**
19 | * Function that processes HTTP GET Request
20 | * @package Bleeding\Routing\Attributes
21 | * @immutable
22 | */
23 | #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
24 | class Get
25 | {
26 | private string $path;
27 |
28 | /**
29 | * Construct GET Controller
30 | *
31 | * @param string $path
32 | */
33 | public function __construct(string $path)
34 | {
35 | $this->path = '/' . trim($path, '/');
36 | }
37 |
38 | /**
39 | * Get path
40 | *
41 | * @return string
42 | */
43 | public function getPath(): string
44 | {
45 | return $this->path;
46 | }
47 |
48 | /**
49 | * get Method name string
50 | *
51 | * @return string
52 | */
53 | public function getMethodName(): string
54 | {
55 | return 'GET';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Bleeding/Routing/Attributes/Post.php:
--------------------------------------------------------------------------------
1 |
7 | * @copyright 2020- Masaru Yamagishi
8 | */
9 |
10 | declare(strict_types=1);
11 |
12 | namespace Bleeding\Routing\Attributes;
13 |
14 | use Attribute;
15 |
16 | use function trim;
17 |
18 | /**
19 | * Function that processes HTTP POST Request
20 | * @package Bleeding\Routing\Attributes
21 | * @immutable
22 | */
23 | #[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
24 | class Post
25 | {
26 | private string $path;
27 |
28 | /**
29 | * Construct GET Controller
30 | *
31 | * @param string $path
32 | */
33 | public function __construct(string $path)
34 | {
35 | $this->path = '/' . trim($path, '/');
36 | }
37 |
38 | /**
39 | * Get path
40 | *
41 | * @return string
42 | */
43 | public function getPath(): string
44 | {
45 | return $this->path;
46 | }
47 |
48 | /**
49 | * get Method name string
50 | *
51 | * @return string
52 | */
53 | public function getMethodName(): string
54 | {
55 | return 'POST';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Edge/Controllers/Users/postLogin.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | use Bleeding\Http\Exceptions\BadRequestException;
11 | use Bleeding\Routing\Attributes\Post;
12 | use Edge\User\LoginService;
13 | use Psr\Http\Message\ServerRequestInterface;
14 | use Respect\Validation\Validator as v;
15 |
16 | return
17 | #[Post('/users/login')]
18 | function (ServerRequestInterface $request, LoginService $loginService) {
19 | $body = $request->getParsedBody();
20 | $username = $body['username'] ?? null;
21 | $rawPassword = $body['password'] ?? null;
22 |
23 | $usernameValidator = v::alnum()->noWhitespace()->length(3, 20);
24 | $rawPasswordValidator = v::alnum()->noWhitespace()->length(6, 64);
25 |
26 | if (!$usernameValidator->validate($username)) {
27 | throw BadRequestException::createWithMessage('ユーザ名は半角英数 3 ~ 20 文字で入力してください');
28 | } elseif (!$rawPasswordValidator->validate($rawPassword)) {
29 | throw BadRequestException::createWithMessage('パスワードは半角英数 6 ~ 64 文字で入力してください');
30 | }
31 |
32 | /**
33 | * @psalm-suppress PossiblyNullArgument
34 | */
35 | return $loginService($username, $rawPassword);
36 | };
37 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/Attributes/GetTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing\Attributes;
11 |
12 | use Bleeding\Routing\Attributes\Get;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Routing\Attributes
17 | * @coversDefaultClass \Bleeding\Routing\Attributes\Get
18 | */
19 | final class GetTest extends TestCase
20 | {
21 | /**
22 | * @test
23 | * @covers ::__construct
24 | */
25 | public function testConstruct(): void
26 | {
27 | $attr = new Get('/');
28 |
29 | $this->assertInstanceOf(Get::class, $attr);
30 | }
31 |
32 | /**
33 | * @test
34 | * @covers ::__construct
35 | * @covers ::getPath
36 | */
37 | public function testGetPath(): void
38 | {
39 | $attr = new Get('foo/bar/');
40 |
41 | $this->assertSame('/foo/bar', $attr->getPath());
42 | }
43 |
44 | /**
45 | * @test
46 | * @covers ::__construct
47 | * @covers ::getMethodName
48 | */
49 | public function testGetMethodName(): void
50 | {
51 | $attr = new Get('');
52 |
53 | $this->assertSame('GET', $attr->getMethodName());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/Attributes/PostTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing\Attributes;
11 |
12 | use Bleeding\Routing\Attributes\Post;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Routing\Attributes
17 | * @coversDefaultClass \Bleeding\Routing\Attributes\Post
18 | */
19 | final class PostTest extends TestCase
20 | {
21 | /**
22 | * @test
23 | * @covers ::__construct
24 | */
25 | public function testConstruct(): void
26 | {
27 | $attr = new Post('/');
28 |
29 | $this->assertInstanceOf(Post::class, $attr);
30 | }
31 |
32 | /**
33 | * @test
34 | * @covers ::__construct
35 | * @covers ::getPath
36 | */
37 | public function testPostPath(): void
38 | {
39 | $attr = new Post('foo/bar/');
40 |
41 | $this->assertSame('/foo/bar', $attr->getPath());
42 | }
43 |
44 | /**
45 | * @test
46 | * @covers ::__construct
47 | * @covers ::getMethodName
48 | */
49 | public function testPostMethodName(): void
50 | {
51 | $attr = new Post('');
52 |
53 | $this->assertSame('POST', $attr->getMethodName());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Edge/User/UserEntity.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\User;
11 |
12 | use JsonSerializable;
13 |
14 | /**
15 | * @package Edge\User
16 | */
17 | final class UserEntity implements JsonSerializable
18 | {
19 | /** @var int $id */
20 | private int $id;
21 |
22 | /** @var string $username */
23 | private string $username;
24 |
25 | /** @var string $hashedPassword */
26 | private string $hashedPassword;
27 |
28 | /**
29 | * @param int $id
30 | * @param string $username
31 | * @param string $hashedPassword
32 | */
33 | public function __construct(int $id, string $username, string $hashedPassword)
34 | {
35 | $this->id = $id;
36 | $this->username = $username;
37 | $this->hashedPassword = $hashedPassword;
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public function jsonSerialize(): array
44 | {
45 | return [
46 | 'id' => $this->id,
47 | 'username' => $this->username,
48 | ];
49 | }
50 |
51 | /**
52 | * @return string
53 | */
54 | public function getHashedPassword(): string
55 | {
56 | return $this->hashedPassword;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/RouteTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing;
11 |
12 | use Bleeding\Routing\Route;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Routing
17 | * @coversDefaultClass \Bleeding\Routing\Route
18 | * @immutable
19 | */
20 | final class RouteTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | * @covers ::__construct
25 | * @covers ::getPath
26 | * @covers ::getMethod
27 | * @covers ::getFunc
28 | * @covers ::getFilePath
29 | * @covers ::getMiddlewares
30 | */
31 | public function testConstruct(): void
32 | {
33 | $route = new Route(
34 | $path = '/',
35 | $method = 'GET',
36 | $func = fn() => true,
37 | $filePath = 'path',
38 | $middlewares = []
39 | );
40 |
41 | $this->assertInstanceOf(Route::class, $route);
42 | $this->assertSame($path, $route->getPath());
43 | $this->assertSame($method, $route->getMethod());
44 | $this->assertSame($func, $route->getFunc());
45 | $this->assertSame($filePath, $route->getFilePath());
46 | $this->assertSame($middlewares, $route->getMiddlewares());
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Bleeding/Support/ClockTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Support;
11 |
12 | use Bleeding\Support\Clock;
13 | use Cake\Chronos\Chronos;
14 | use Cake\Chronos\ChronosInterface;
15 | use Tests\TestCase;
16 |
17 | /**
18 | * @package Tests\Bleeding\Support
19 | * @coversDefaultClass \Bleeding\Support\Clock
20 | */
21 | final class ClockTest extends TestCase
22 | {
23 | /**
24 | * @return void
25 | */
26 | public function tearDown(): void
27 | {
28 | Chronos::setTestNow();
29 | }
30 |
31 | /**
32 | * @test
33 | * @covers ::entry
34 | */
35 | public function testEntry(): void
36 | {
37 | $instance = Clock::entry();
38 |
39 | $this->assertInstanceOf(ChronosInterface::class, $instance);
40 | $this->assertEquals('1970-01-01T00:00:00+00:00', $instance->toIso8601String());
41 | }
42 |
43 | /**
44 | * @test
45 | * @covers ::now
46 | */
47 | public function testNow(): void
48 | {
49 | $str = '2020-01-01T09:00:00+09:00';
50 | Chronos::setTestNow($str);
51 | $instance = Clock::now();
52 |
53 | $this->assertInstanceOf(ChronosInterface::class, $instance);
54 | $this->assertEquals($str, $instance->toIso8601String());
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/CollectRouteTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 | use Bleeding\Routing\CollectRoute;
14 | use LogicException;
15 | use Psr\Container\ContainerInterface;
16 | use Psr\Http\Server\RequestHandlerInterface;
17 | use Psr\Http\Server\MiddlewareInterface;
18 | use Tests\TestCase;
19 |
20 | /**
21 | * @package Tests\Bleeding\Routing
22 | * @immutable
23 | * @coversDefaultClass \Bleeding\Routing\CollectRoute
24 | */
25 | final class CollectRouteTest extends TestCase
26 | {
27 | /**
28 | * @test
29 | * @covers ::collect
30 | * @covers ::checkFile
31 | * @covers ::getMiddlewares
32 | * @covers ::getAttribute
33 | * @uses \Bleeding\Http\Attributes\Middleware
34 | * @uses \Bleeding\Routing\Attributes\Get
35 | * @uses \Bleeding\Routing\Attributes\Post
36 | * @uses \Bleeding\Routing\Route
37 | */
38 | public function testCollect(): void
39 | {
40 | $baseDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Stub', 'Routing']);
41 | $paths = CollectRoute::collect($baseDir);
42 |
43 | $this->assertSame(['/', '/middleware'], array_keys($paths));
44 | $this->assertSame(['POST', 'GET', 'HEAD'], array_keys($paths['/']));
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Bleeding/Applications/ContainerFactoryTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Applications;
11 |
12 | use Bleeding\Applications\ContainerFactory;
13 | use Psr\Container\ContainerInterface;
14 | use Tests\TestCase;
15 |
16 | use function putenv;
17 | use function unlink;
18 |
19 | /**
20 | * @package Tests\Bleeding\Applications
21 | * @coversDefaultClass \Bleeding\Applications\ContainerFactory
22 | * @immutable
23 | */
24 | final class ContainerFactoryTest extends TestCase
25 | {
26 | /**
27 | * @test
28 | * @covers ::create
29 | * @covers ::addDefinitions
30 | * @covers ::resolveDefinitionsPath
31 | */
32 | public function testCreate(): void
33 | {
34 | $container = ContainerFactory::create();
35 |
36 | $this->assertInstanceOf(ContainerInterface::class, $container);
37 | }
38 |
39 | /**
40 | * @test
41 | * @covers ::create
42 | * @covers ::addDefinitions
43 | * @covers ::resolveDefinitionsPath
44 | */
45 | public function testCreateWithCache(): void
46 | {
47 | putenv('DEBUG_MODE=false');
48 | $cacheDir = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'cache']);
49 | @unlink($cacheDir . '/CompiledContainer.php');
50 |
51 | $container = ContainerFactory::create($cacheDir);
52 |
53 | $this->assertInstanceOf(ContainerInterface::class, $container);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Bleeding/Applications/WebApplicationTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Applications;
11 |
12 | use Bleeding\Applications\WebApplication;
13 | use DI\Container;
14 | use Laminas\Diactoros\Response;
15 | use Monolog\Handler\TestHandler;
16 | use Monolog\Logger;
17 | use Psr\Log\LoggerInterface;
18 | use Tests\TestCase;
19 |
20 | /**
21 | * @package Tests\Bleeding\Applications
22 | * @coversDefaultClass \Bleeding\Applications\WebApplication
23 | * @immutable
24 | */
25 | final class WebApplicationTest extends TestCase
26 | {
27 | /**
28 | * @test
29 | * @covers ::createLogger
30 | * @covers ::createContainer
31 | * @covers ::run
32 | * @covers ::createServerRequest
33 | * @uses \Bleeding\Applications\ContainerFactory
34 | * @uses \Bleeding\Applications\LoggerFactory
35 | * @uses \Bleeding\Applications\ErrorHandler
36 | * @uses \Bleeding\Http\ServerRequestFactory
37 | * @uses \Bleeding\Routing\MiddlewareResolver
38 | */
39 | public function testRun(): void
40 | {
41 | $app = new class extends WebApplication {
42 | protected function createProcessQueue(Container $container): array {
43 | return [
44 | fn () => new Response(),
45 | ];
46 | }
47 | };
48 |
49 | $exitCode = $app->run();
50 |
51 | $this->assertSame(0, $exitCode);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Bleeding/Applications/ContainerFactory.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | use DI\Container;
13 | use DI\ContainerBuilder;
14 |
15 | /**
16 | * ContainerFactory
17 | * @package Bleeding\Applications
18 | */
19 | class ContainerFactory
20 | {
21 | /**
22 | * Create Container
23 | *
24 | * @param ?string $cacheDir
25 | * @return Container
26 | */
27 | public static function create(?string $cacheDir = null): Container
28 | {
29 | $builder = new ContainerBuilder();
30 |
31 | if (!is_null($cacheDir) && getenv('DEBUG_MODE') !== 'true') {
32 | // optimization
33 | $builder->enableCompilation($cacheDir);
34 | }
35 |
36 | static::addDefinitions($builder);
37 |
38 | return $builder->build();
39 | }
40 |
41 | /**
42 | * Add container definitions
43 | *
44 | * @param ContainerBuilder $builder
45 | * @return void
46 | */
47 | protected static function addDefinitions(ContainerBuilder $builder): void
48 | {
49 | $builder->addDefinitions(self::resolveDefinitionsPath('Bleeding', 'Http'));
50 | }
51 |
52 | /**
53 | * resolve definitions path
54 | *
55 | * @param string[] $args
56 | * @return string
57 | */
58 | private static function resolveDefinitionsPath(string ...$args): string
59 | {
60 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', ...$args, 'definitions.php']);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Bleeding/Http/Exceptions/InternalServerErrorException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Exceptions;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 | use Throwable;
14 |
15 | /**
16 | * Raw HTTP Exception
17 | * @package Bleeding\Http\Exceptions
18 | */
19 | class InternalServerErrorException extends RuntimeException
20 | {
21 | /** @var string MESSAGE */
22 | protected const MESSAGE = 'Internal Server Error';
23 |
24 | /** @var int CODE */
25 | protected const CODE = 500;
26 |
27 | /**
28 | * Create contextual Exception
29 | *
30 | * @param array $context
31 | * @param ?Throwable $previous
32 | * @return static
33 | */
34 | public static function createWithMessage(
35 | string $message,
36 | array $context = [],
37 | ?Throwable $previous = null
38 | ): static {
39 | $exception = new static($message, static::CODE, $previous);
40 | $exception->setContext($context);
41 |
42 | return $exception;
43 | }
44 |
45 | /**
46 | * Create contextual Exception
47 | *
48 | * @param array $context
49 | * @param ?Throwable $previous
50 | * @return static
51 | */
52 | public static function createWithContext(
53 | array $context = [],
54 | ?Throwable $previous = null
55 | ): static {
56 | $exception = new static(static::MESSAGE, static::CODE, $previous);
57 | $exception->setContext($context);
58 |
59 | return $exception;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Edge/Applications/WebApplication.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Applications;
11 |
12 | use Bleeding\Applications\WebApplication as WebApplicationBase;
13 | use Bleeding\Http\Middlewares\ParseBodyMiddleware;
14 | use Bleeding\Http\Middlewares\ProcessErrorMiddleware;
15 | use Bleeding\Routing\RoutingResolver;
16 | use DI\Container;
17 |
18 | use const DIRECTORY_SEPARATOR;
19 |
20 | /**
21 | * @package Edge\Applications
22 | */
23 | class WebApplication extends WebApplicationBase
24 | {
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public function createContainer(): Container
29 | {
30 | return ContainerFactory::create();
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function getBaseDirectory(): string
37 | {
38 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..']);
39 | }
40 |
41 | public function getCacheDirectory(): string
42 | {
43 | return $this->getBaseDirectory() . DIRECTORY_SEPARATOR . 'cache';
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function createProcessQueue(Container $container): array
50 | {
51 | $baseDir = implode(DIRECTORY_SEPARATOR, [$this->getBaseDirectory(), 'Edge', 'Controllers']);
52 |
53 | // register main Middlewares
54 | $queue = [
55 | ProcessErrorMiddleware::class,
56 | ParseBodyMiddleware::class,
57 | new RoutingResolver($baseDir, $container),
58 | ];
59 |
60 | return $queue;
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/Edge/Repositories/PDOUserRepository.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Edge\Repositories;
11 |
12 | use Edge\User\IUserRepository;
13 | use Edge\User\UserEntity;
14 |
15 | /**
16 | * @package Edge\Repositories
17 | */
18 | final class PDOUserRepository implements IUserRepository
19 | {
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function findById(int $id): ?UserEntity
24 | {
25 | throw new \LogicException('Not implemented');
26 | // $sql = 'SELECT * FROM `users` WHERE `id` = :id;';
27 | // $stmt = $this->connection->user($id)->prepare($sql);
28 | // $stmt->bindParam(':id', $id, PDO::PARAM_INT);
29 | // $stmt->execute();
30 | // $result = $stmt->fetch();
31 | // if (!$result) {
32 | // return null;
33 | // }
34 | // return new UserEntity((int)$result['id'], $result['username'], $result['password']);
35 |
36 | // $result = $this->connection
37 | // ->user($id)
38 | // ->table('users')
39 | // ->find($id);
40 | // if (!$result) {
41 | // return null;
42 | // }
43 | // return new UserEntity((int)$result['id'], $result['username'], $result['password']);
44 | }
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | public function findByName(string $username): ?UserEntity
50 | {
51 | throw new \LogicException('Not implemented');
52 | // return $this->connection
53 | // ->userAll()
54 | // ->table('users')
55 | // ->where('username', $username)
56 | // ->first();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tests/Bleeding/Exceptions/RuntimeExceptionTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Exceptions;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 | use RuntimeException as RuntimeExceptionBase;
14 | use Tests\TestCase;
15 |
16 | /**
17 | * @package Tests\Bleeding\Exceptions
18 | * @coversDefaultClass \Bleeding\Exceptions\RuntimeException
19 | */
20 | final class RuntimeExceptionTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | * @covers ::__construct
25 | */
26 | public function testConstruct()
27 | {
28 | $message = 'test';
29 | $code = 200;
30 | $previous = new RuntimeExceptionBase('foo');
31 | $e = new RuntimeException($message, $code, $previous);
32 |
33 | $this->assertInstanceOf(RuntimeException::class, $e);
34 | $this->assertInstanceOf(RuntimeExceptionBase::class, $e);
35 | }
36 |
37 | /**
38 | * @test
39 | * @covers ::__construct
40 | * @covers ::getContext
41 | */
42 | public function testGetContext()
43 | {
44 | $e = new RuntimeException('test', 200);
45 | $this->assertEquals([], $e->getContext());
46 | }
47 |
48 | /**
49 | * @test
50 | * @covers ::__construct
51 | * @covers ::create
52 | * @covers ::setContext
53 | * @covers ::getContext
54 | */
55 | public function testCreate()
56 | {
57 | $e = RuntimeException::create('message', 100, ['context' => 'is fine']);
58 |
59 | $this->assertInstanceOf(RuntimeException::class, $e);
60 | $this->assertEquals(['context' => 'is fine'], $e->getContext());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Bleeding/Exceptions/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Exceptions;
11 |
12 | use RuntimeException as RuntimeExceptionBase;
13 | use Throwable;
14 |
15 | /**
16 | * Raw Runtime Exception
17 | * @package Bleeding\Exceptions
18 | */
19 | class RuntimeException extends RuntimeExceptionBase
20 | {
21 | /** @var array $context */
22 | protected array $context = [];
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function __construct(string $message, int $code, ?Throwable $previous = null)
28 | {
29 | parent::__construct($message, $code, $previous);
30 | }
31 |
32 | /**
33 | * Set exception context
34 | *
35 | * @param array $context
36 | * @return void
37 | */
38 | protected function setContext(array $context): void
39 | {
40 | $this->context = $context;
41 | }
42 |
43 | /**
44 | * Get exception context
45 | *
46 | * @return array
47 | */
48 | public function getContext(): array
49 | {
50 | return $this->context;
51 | }
52 |
53 | /**
54 | * Create contextual Exception
55 | *
56 | * @param string $message
57 | * @param int $code
58 | * @param array $context
59 | * @param ?Throwable $previous
60 | * @return static
61 | */
62 | public static function create(
63 | string $message,
64 | int $code,
65 | array $context = [],
66 | ?Throwable $previous = null
67 | ): static {
68 | $exception = new static($message, $code, $previous);
69 | $exception->setContext($context);
70 |
71 | return $exception;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/container/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | worker_processes auto;
3 | error_log /dev/stderr warn;
4 | pid /var/run/nginx.pid;
5 |
6 | # Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
7 | include /usr/share/nginx/modules/*.conf;
8 |
9 | events {
10 | worker_connections 1024;
11 | }
12 |
13 | http {
14 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
15 | '$status $body_bytes_sent "$http_referer" '
16 | '"$http_user_agent" "$http_x_forwarded_for"';
17 |
18 | access_log /dev/stdout main;
19 |
20 | include mime.types;
21 | default_type application/octet-stream;
22 | sendfile on;
23 | keepalive_timeout 65;
24 |
25 | add_header X-Frame-Options DENY;
26 | add_header X-XSS-Protection "1; mode=block";
27 | add_header X-Content-Type-Options "nosniff";
28 |
29 | # Load modular configuration files from the /etc/nginx/conf.d directory.
30 | # See http://nginx.org/en/docs/ngx_core_module.html#include
31 | # for more information.
32 | include /etc/nginx/conf.d/*.conf;
33 |
34 | index index.php;
35 |
36 | server {
37 | listen 443 ssl http2;
38 | server_name bleeding.example.com
39 |
40 | ssl_protocols TLSv1.3 TLSv1.2;
41 | ssl_certificate cert.pem;
42 | ssl_certificate_key cert.key;
43 | ssl_prefer_server_ciphers on;
44 |
45 | location / {
46 | root /opt/app/public;
47 | index index.php index.html index.htm;
48 | try_files $uri $uri/ /index.php?$query_string;
49 |
50 | location ~ \.php$ {
51 | fastcgi_pass unix:/var/run/php-fpm/www.sock;
52 | fastcgi_index index.php;
53 | include fastcgi_params;
54 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # App
2 |
3 | .env
4 | _wildcard.example.com.pem
5 | _wildcard.example.com-key.pem
6 | vendor
7 | .phpunit.cache
8 | composer.lock
9 |
10 | # End App
11 |
12 | # Created by https://www.toptal.com/developers/gitignore/api/code,macos,windows,linux
13 | # Edit at https://www.toptal.com/developers/gitignore?templates=code,macos,windows,linux
14 |
15 | ### Code ###
16 | .vscode/*
17 | !.vscode/settings.json
18 | !.vscode/tasks.json
19 | !.vscode/launch.json
20 | !.vscode/extensions.json
21 | *.code-workspace
22 |
23 | ### Linux ###
24 | *~
25 |
26 | # temporary files which can be created if a process still has a handle open of a deleted file
27 | .fuse_hidden*
28 |
29 | # KDE directory preferences
30 | .directory
31 |
32 | # Linux trash folder which might appear on any partition or disk
33 | .Trash-*
34 |
35 | # .nfs files are created when an open file is removed but is still being accessed
36 | .nfs*
37 |
38 | ### macOS ###
39 | # General
40 | .DS_Store
41 | .AppleDouble
42 | .LSOverride
43 |
44 | # Icon must end with two \r
45 | Icon
46 |
47 | # Thumbnails
48 | ._*
49 |
50 | # Files that might appear in the root of a volume
51 | .DocumentRevisions-V100
52 | .fseventsd
53 | .Spotlight-V100
54 | .TemporaryItems
55 | .Trashes
56 | .VolumeIcon.icns
57 | .com.apple.timemachine.donotpresent
58 |
59 | # Directories potentially created on remote AFP share
60 | .AppleDB
61 | .AppleDesktop
62 | Network Trash Folder
63 | Temporary Items
64 | .apdisk
65 |
66 | ### Windows ###
67 | # Windows thumbnail cache files
68 | Thumbs.db
69 | Thumbs.db:encryptable
70 | ehthumbs.db
71 | ehthumbs_vista.db
72 |
73 | # Dump file
74 | *.stackdump
75 |
76 | # Folder config file
77 | [Dd]esktop.ini
78 |
79 | # Recycle Bin used on file shares
80 | $RECYCLE.BIN/
81 |
82 | # Windows Installer files
83 | *.cab
84 | *.msi
85 | *.msix
86 | *.msm
87 | *.msp
88 |
89 | # Windows shortcuts
90 | *.lnk
91 |
92 | # End of https://www.toptal.com/developers/gitignore/api/code,macos,windows,linux
93 |
--------------------------------------------------------------------------------
/tests/Bleeding/Http/Exceptions/InternalServerErrorExceptionTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Http\Exceptions;
11 |
12 | use Bleeding\Http\Exceptions\InternalServerErrorException;
13 | use Tests\TestCase;
14 |
15 | /**
16 | * @package Tests\Bleeding\Http\Exceptions
17 | * @coversDefaultClass \Bleeding\Http\Exceptions\InternalServerErrorException
18 | * @immutable
19 | */
20 | final class InternalServerErrorExceptionTest extends TestCase
21 | {
22 | /**
23 | * @test
24 | * @covers ::__construct
25 | * @covers ::createWithMessage
26 | * @covers ::setContext
27 | * @covers ::getContext
28 | */
29 | public function testCreateWithMessage(): void
30 | {
31 | $exception = InternalServerErrorException::createWithMessage(
32 | $message = 'error',
33 | $context = ['foo' => 'bar'],
34 | $previous = null
35 | );
36 |
37 | $this->assertSame('error', $exception->getMessage());
38 | $this->assertSame(500, $exception->getCode());
39 | $this->assertSame(['foo' => 'bar'], $exception->getContext());
40 | $this->assertSame(null, $exception->getPrevious());
41 | }
42 |
43 | /**
44 | * @test
45 | * @covers ::__construct
46 | * @covers ::createWithContext
47 | * @covers ::setContext
48 | * @covers ::getContext
49 | */
50 | public function testCreateWithContext(): void
51 | {
52 | $exception = InternalServerErrorException::createWithContext(
53 | $context = ['foo' => 'bar'],
54 | $previous = null
55 | );
56 |
57 | $this->assertSame('Internal Server Error', $exception->getMessage());
58 | $this->assertSame(['foo' => 'bar'], $exception->getContext());
59 | $this->assertSame(null, $exception->getPrevious());
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Bleeding/Routing/MiddlewareResolver.php:
--------------------------------------------------------------------------------
1 | isValidInstance($entry)) {
42 | return $entry;
43 | }
44 | if (!is_string($entry) || !class_exists($entry)) {
45 | throw RuntimeException::create('Cannot resolve Middleware entry: ' . $entry, 1);
46 | }
47 | if (!$this->container->has($entry)) {
48 | throw new LogicException('Unknown Middleware in Container: ' . $entry);
49 | }
50 | $entryInstance = $this->container->get($entry);
51 | assert($this->isValidInstance($entryInstance), 'Assert $entryInstance is valid middleware');
52 | return $entryInstance;
53 | };
54 | }
55 |
56 | /**
57 | * validate entry is valid instance
58 | * @param mixed $entry
59 | * @return bool
60 | */
61 | private function isValidInstance(mixed $entry): bool
62 | {
63 | return $entry instanceof MiddlewareInterface ||
64 | $entry instanceof RequestHandlerInterface ||
65 | is_callable($entry);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bleeding Edge PHP 8 Framework
2 |
3 | **Caution: This framework is under development. You should not use in production!**
4 |
5 | ## Introduction
6 |
7 | - PHP 8 Ready!
8 | - Thin framework
9 | - No [nikic/fast-route](https://github.com/nikic/FastRoute)
10 | - No route parameters
11 | - GET & POST HTTP method only(RESTful is too complex)
12 | - Functional controller(No Instance or state needed)
13 | - Controller attributes(Method, Path, Middlewares)
14 | - Functional command with attributes, powered by [Silly](https://github.com/mnapoli/silly)
15 | - Accepts `application/json` first, `multipart/form-data` second
16 | - returns only `application/json`
17 | - PHP Standard Recommendation(PSR) first
18 | - PSR-3 Log ready, powered by [monolog](https://github.com/Seldaek/monolog)
19 | - PSR-4 Autoload ready, powered by [composer v2](https://getcomposer.org/)
20 | - PSR-7, PSR-17 HTTP ready, powered by [laminas-diactoros](https://docs.laminas.dev/laminas-diactoros/)
21 | - PSR-11 DI ready, powered by [PHP-DI 7(beta)](https://php-di.org/)
22 | - PSR-12 ready, powered by [PHP CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)
23 | - PSR-15 RequestHandler, Middleware ready, powered by [Relay](http://relayphp.com/)
24 |
25 | ## Developer Install
26 |
27 | - clone this repository
28 | - [Install mkcert](https://github.com/FiloSottile/mkcert)
29 | - Install docker & docker-compose in any style
30 |
31 | ```
32 | # installs TLS certification
33 | $ mkcert "*.example.com"
34 |
35 | Created a new certificate valid for the following names 📜
36 | - "*.example.com"
37 |
38 | Reminder: X.509 wildcards only go one level deep, so this won't match a.b.example.com ℹ️
39 |
40 | The certificate is at "./_wildcard.example.com.pem" and the key at "./_wildcard.example.com-key.pem" ✅
41 |
42 | $ cp *.pem container/nginx/
43 | $ cp .env.example .env
44 | $ docker-compose up -d
45 | $ docker-compose exec workspace composer install --ignore-platform-req=php
46 |
47 | # add `docker-ip bleeding.example.com` to hosts file
48 | $ vi /etc/hosts
49 |
50 | $ curl https://bleeding.example.com
51 | {"Hello":"world"}
52 | ```
53 |
--------------------------------------------------------------------------------
/Bleeding/Console/CollectCommand.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Console;
11 |
12 | use Bleeding\Console\Attributes\Command as CommandAttr;
13 | use RecursiveDirectoryIterator;
14 | use RecursiveIteratorIterator;
15 | use ReflectionFunction;
16 | use SplFileInfo;
17 |
18 | use function is_callable;
19 | use function is_null;
20 | use function str_ends_with;
21 |
22 | /**
23 | * @package Bleeding\Console
24 | * @immutable
25 | */
26 | final class CollectCommand
27 | {
28 | /**
29 | * List up all routes
30 | *
31 | * @todo PHP file caching
32 | * @return array
33 | */
34 | public static function collect(string $baseDir): array
35 | {
36 | $iterator = new RecursiveIteratorIterator(
37 | new RecursiveDirectoryIterator($baseDir),
38 | RecursiveIteratorIterator::SELF_FIRST
39 | );
40 |
41 | $commands = [];
42 |
43 | foreach ($iterator as $file) {
44 | $command = self::checkFile($file);
45 |
46 | if (!is_null($command)) {
47 | $commands[] = $command;
48 | }
49 | }
50 |
51 | return $commands;
52 | }
53 |
54 | /**
55 | * @internal
56 | * @param SplFileInfo $file
57 | * @return ?Command
58 | */
59 | private static function checkFile(SplFileInfo $file): ?Command
60 | {
61 | if (!str_ends_with($file->getBaseName(), '.php')) {
62 | return null;
63 | }
64 |
65 | $func = require $file->getRealPath();
66 | assert(is_callable($func), 'command is callable');
67 |
68 | /** @psalm-suppress InvalidArgument */
69 | $ref = new ReflectionFunction($func);
70 | assert(0 < count($ref->getAttributes()), 'command has attribute');
71 |
72 | $attr = null;
73 | if (0 < count($ref->getAttributes(CommandAttr::class))) {
74 | $attr = $ref->getAttributes(CommandAttr::class)[0]->newInstance();
75 | }
76 | assert(!is_null($attr), 'No command attribute has set: ' . $file->getRealPath());
77 |
78 | return new Command($attr->getDefinition(), $func);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | nginx:
4 | image: nginx:1.19.2
5 | restart: always
6 | ports:
7 | - "443:443"
8 | volumes:
9 | - php-fpm-socket:/var/run/php-fpm:cached
10 | - ${APP_PATH-.}/container/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
11 | - ${APP_PATH-.}/container/nginx/_wildcard.example.com.pem:/etc/nginx/cert.pem:ro
12 | - ${APP_PATH-.}/container/nginx/_wildcard.example.com-key.pem:/etc/nginx/cert.key:ro
13 | depends_on:
14 | - web
15 | networks:
16 | - my_network
17 |
18 | web:
19 | build: .
20 | restart: always
21 | volumes:
22 | - php-fpm-socket:/var/run/php-fpm:cached
23 | - ${APP_PATH-.}:/opt/app/:cached
24 | - ${APP_PATH-.}/container/web/www.conf:/usr/local/etc/php-fpm.d/zzz-www.conf:ro
25 | - ${APP_PATH-.}/container/web/php.ini:/usr/local/etc/php/php.ini:ro
26 | depends_on:
27 | - mysql
28 | env_file:
29 | - .env
30 | networks:
31 | - my_network
32 |
33 | workspace:
34 | build: .
35 | working_dir: /opt/app
36 | volumes:
37 | - ${APP_PATH-.}:/opt/app/:cached
38 | - ${APP_PATH-.}/container/web/php.ini:/usr/local/etc/php/php.ini:ro
39 | env_file:
40 | - .env
41 | networks:
42 | - my_network
43 |
44 | mysql:
45 | image: mysql:8.0.21
46 | command: --default-authentication-plugin=mysql_native_password
47 | restart: always
48 | volumes:
49 | - mysql:/var/lib/mysql:cached
50 | environment:
51 | MYSQL_ROOT_PASSWORD: secret
52 | MYSQL_DATABASE: ${MYSQL_DATABASE-bleeding}
53 | MYSQL_USER: ${MYSQL_USER-bleeding}
54 | MYSQL_PASSWORD: ${MYSQL_PASSWORD-secret}
55 | networks:
56 | - my_network
57 |
58 | adminer:
59 | image: adminer:latest
60 | restart: always
61 | ports:
62 | - "8081:8080"
63 | environment:
64 | ADMINER_DESIGN: pappu687
65 | ADMINER_DEFAULT_SERVER: mysql
66 | networks:
67 | - my_network
68 |
69 | networks:
70 | my_network:
71 | volumes:
72 | php-fpm-socket:
73 | mysql:
74 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "il-m-yamagishi/bleeding",
3 | "description": "Bleeding Edge PHP 8 Framework",
4 | "keywords": ["php8", "framework"],
5 | "license": "Apache 2.0",
6 | "homepage": "https://github.com/il-m-yamagishi/bleeding",
7 | "support": {
8 | "issues": "https://github.com/il-m-yamagishi/bleeding/issues",
9 | "source": "https://github.com/il-m-yamagishi/bleeding"
10 | },
11 | "authors": [
12 | {
13 | "name": "Masaru Yamagishi",
14 | "email": "m-yamagishi@infiniteloop.co.jp",
15 | "homepage": "https://cafe-capy.net",
16 | "role": "Author"
17 | }
18 | ],
19 | "require": {
20 | "php": "~8.0-dev",
21 | "ext-json": "*",
22 | "cakephp/chronos": "^2.0",
23 | "laminas/laminas-diactoros": "^2.4",
24 | "mnapoli/silly": "^1.7",
25 | "monolog/monolog": "^2.1",
26 | "narrowspark/http-emitter": "^1.0",
27 | "php-di/php-di": "^7.0.0-beta2",
28 | "psr/container": "^1.0",
29 | "psr/http-factory": "^1.0",
30 | "psr/http-message": "^1.0",
31 | "psr/http-server-handler": "^1.0",
32 | "relay/relay": "~2.0",
33 | "respect/validation": "^2.1",
34 | "riverline/multipart-parser": "^2.0"
35 | },
36 | "require-dev": {
37 | "phpunit/phpunit": "^9.0",
38 | "squizlabs/php_codesniffer": "^3.5",
39 | "vimeo/psalm": "^4.0.0"
40 | },
41 | "autoload": {
42 | "psr-4": {
43 | "Bleeding\\": "Bleeding/",
44 | "Edge\\": "Edge/"
45 | }
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "Tests\\": "tests"
50 | }
51 | },
52 | "scripts": {
53 | "phpcs": "phpcs",
54 | "phpcbf": "phpcbf",
55 | "test": "phpunit",
56 | "coverage": "phpdbg -qrr vendor/bin/phpunit --coverage-html=cache/coverage",
57 | "psalm": "psalm"
58 | },
59 | "scripts-descriptions": {
60 | "phpcs": "Run PHP CodeSniffer for PSR-12",
61 | "phpcbf": "Run autofix to PSR-12-valid",
62 | "test": "Run test",
63 | "coverage": "Run test and show coverage",
64 | "psalm": "Run psalm analysis"
65 | },
66 | "config": {
67 | "sort-packages": true
68 | },
69 | "minimum-stability": "dev",
70 | "prefer-stable": true
71 | }
72 |
--------------------------------------------------------------------------------
/Bleeding/Routing/Route.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Routing;
11 |
12 | use function trim;
13 | use function strtoupper;
14 |
15 | /**
16 | * Route
17 | *
18 | * @package Bleeding\Routing
19 | * @immutable
20 | */
21 | final class Route
22 | {
23 | /** @var string */
24 | private string $path;
25 |
26 | /** @var string */
27 | private string $method;
28 |
29 | /** @var callable */
30 | private $func;
31 |
32 | /** @var string function path for caching */
33 | private string $filePath;
34 |
35 | /** @var string[] */
36 | private array $middlewares;
37 |
38 | /**
39 | * @param string $path
40 | * @param string $method
41 | * @param callable $func
42 | * @param string $filePath
43 | * @param string[]|string $middlewares
44 | */
45 | public function __construct(
46 | string $path,
47 | string $method,
48 | callable $func,
49 | string $filePath,
50 | $middlewares = []
51 | ) {
52 | $this->path = '/' . trim($path, '/');
53 | $this->method = strtoupper($method);
54 | $this->func = $func;
55 | $this->filePath = $filePath;
56 | $this->middlewares = (array)$middlewares;
57 | }
58 |
59 | /**
60 | * get Path
61 | * @return string
62 | */
63 | public function getPath(): string
64 | {
65 | return $this->path;
66 | }
67 |
68 | /**
69 | * get HTTP method
70 | * @return string
71 | */
72 | public function getMethod(): string
73 | {
74 | return $this->method;
75 | }
76 |
77 | /**
78 | * get controller function
79 | * @return callable
80 | */
81 | public function getFunc(): callable
82 | {
83 | return $this->func;
84 | }
85 |
86 | /**
87 | * get function filepath
88 | * @return string
89 | */
90 | public function getFilePath(): string
91 | {
92 | return $this->filePath;
93 | }
94 |
95 | /**
96 | * get route specific middlewares
97 | * @return string[]
98 | */
99 | public function getMiddlewares(): array
100 | {
101 | return $this->middlewares;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/Bleeding/Applications/ConsoleApplicationTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Applications;
11 |
12 | use Bleeding\Applications\ConsoleApplication;
13 | use Monolog\Handler\TestHandler;
14 | use Monolog\Logger;
15 | use Psr\Log\LoggerInterface;
16 | use Symfony\Component\Console\Input\ArgvInput;
17 | use Symfony\Component\Console\Output\NullOutput;
18 | use Tests\TestCase;
19 |
20 | /**
21 | * @package Tests\Bleeding\Applications
22 | * @coversDefaultClass \Bleeding\Applications\ConsoleApplication
23 | * @immutable
24 | */
25 | final class ConsoleApplicationTest extends TestCase
26 | {
27 | /**
28 | * @test
29 | * @covers ::__construct
30 | * @covers ::createLogger
31 | * @covers ::createContainer
32 | * @covers ::run
33 | * @uses \Bleeding\Applications\ContainerFactory
34 | * @uses \Bleeding\Applications\ErrorHandler
35 | * @uses \Bleeding\Console\Attributes\Command
36 | * @uses \Bleeding\Console\CollectCommand
37 | * @uses \Bleeding\Console\Command
38 | * @uses \Silly\Application
39 | */
40 | public function testRun(): void
41 | {
42 | $app = new class (new ArgvInput(['bleeding', 'hello']), new NullOutput()) extends ConsoleApplication {
43 | protected function getCommandDirectory(): string {
44 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Stub', 'Console']);
45 | }
46 | };
47 |
48 | $exitCode = $app->run();
49 |
50 | $this->assertSame(0, $exitCode);
51 | }
52 |
53 | /**
54 | * @test
55 | * @covers ::__construct
56 | * @covers ::createLogger
57 | * @covers ::createContainer
58 | * @covers ::run
59 | * @uses \Bleeding\Applications\ContainerFactory
60 | * @uses \Bleeding\Applications\ErrorHandler
61 | * @uses \Bleeding\Console\Attributes\Command
62 | * @uses \Bleeding\Console\CollectCommand
63 | * @uses \Bleeding\Console\Command
64 | * @uses \Silly\Application
65 | */
66 | public function testRunFailed(): void
67 | {
68 | $app = new class (new ArgvInput(['bleeding', 'show-error']), new NullOutput()) extends ConsoleApplication {
69 | protected function getCommandDirectory(): string {
70 | return implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', 'Stub', 'Console']);
71 | }
72 | };
73 |
74 | $exitCode = $app->run();
75 |
76 | $this->assertSame(2, $exitCode);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/Bleeding/Applications/ErrorHandlerTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Applications;
11 |
12 | use Bleeding\Applications\ErrorHandler;
13 | use Monolog\Handler\TestHandler;
14 | use Monolog\Logger;
15 | use Psr\Log\LoggerInterface;
16 | use Tests\TestCase;
17 |
18 | use function trigger_error;
19 |
20 | use const E_USER_NOTICE;
21 |
22 | /**
23 | * @package Tests\Bleeding\Applications
24 | * @coversDefaultClass \Bleeding\Applications\ErrorHandler
25 | * @immutable
26 | */
27 | final class ErrorHandlerTest extends TestCase
28 | {
29 | /**
30 | * @test
31 | * @covers ::__construct
32 | * @covers ::setErrorHandler
33 | * @covers ::restoreErrorHandler
34 | */
35 | public function testConstruct(): void
36 | {
37 | $errorHandler = new ErrorHandler(new Logger('ErrorHandlerTest::testConstruct'));
38 | $errorHandler->setErrorHandler();
39 | $errorHandler->restoreErrorHandler();
40 |
41 | $this->assertInstanceOf(ErrorHandler::class, $errorHandler);
42 | }
43 |
44 | /**
45 | * @test
46 | * @covers ::__construct
47 | * @covers ::setErrorHandler
48 | * @covers ::restoreErrorHandler
49 | * @covers ::handle
50 | */
51 | public function testHandle(): void
52 | {
53 | $testHandler = new TestHandler();
54 | $logger = new Logger('ErrorHandlerTest::testHandle');
55 | $logger->pushHandler($testHandler);
56 | $errorHandler = new ErrorHandler($logger);
57 |
58 | $error = error_reporting();
59 | error_reporting(0);
60 | $errorHandler->setErrorHandler();
61 | trigger_error('Unknown Error', E_USER_NOTICE);
62 | $errorHandler->restoreErrorHandler();
63 | error_reporting($error);
64 |
65 | $this->assertTrue($testHandler->hasDebugThatContains('Unknown Error'));
66 | }
67 |
68 | /**
69 | * @test
70 | * @covers ::__construct
71 | * @covers ::setErrorHandler
72 | * @covers ::restoreErrorHandler
73 | * @covers ::handle
74 | */
75 | public function testSuppressed(): void
76 | {
77 | $testHandler = new TestHandler();
78 | $logger = new Logger('ErrorHandlerTest::testHandle');
79 | $logger->pushHandler($testHandler);
80 | $errorHandler = new ErrorHandler($logger);
81 |
82 | $errorHandler->setErrorHandler();
83 | @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'foo');
84 | $errorHandler->restoreErrorHandler();
85 |
86 | $this->assertTrue($testHandler->hasErrorThatContains('No such file or directory'));
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Bleeding/Http/Middlewares/ParseBodyMiddleware.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Middlewares;
11 |
12 | use Bleeding\Http\Exceptions\BadRequestException;
13 | use JsonException;
14 | use Psr\Http\Message\ServerRequestInterface;
15 | use Psr\Http\Message\ResponseInterface;
16 | use Psr\Http\Server\MiddlewareInterface;
17 | use Psr\Http\Server\RequestHandlerInterface;
18 | use Riverline\MultiPartParser\Converters\PSR7;
19 |
20 | use function json_decode;
21 | use function parse_str;
22 | use function str_ends_with;
23 | use function str_starts_with;
24 |
25 | use const JSON_THROW_ON_ERROR;
26 |
27 | /**
28 | * Parse request body
29 | * @package Bleeding\Http\Middlewares
30 | * @immutable
31 | */
32 | final class ParseBodyMiddleware implements MiddlewareInterface
33 | {
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
38 | {
39 | $contentType = $request->getHeaderLine('Content-Type');
40 |
41 | $request = match (true) {
42 | str_ends_with($contentType, 'json') => $this->parseJson($request),
43 | str_starts_with($contentType, 'multipart') => $this->parseMultipart($request),
44 | str_ends_with($contentType, 'x-www-form-urlencoded') => $this->parseForm($request),
45 | default => $request,
46 | };
47 |
48 | return $handler->handle($request);
49 | }
50 |
51 | /**
52 | * Parse JSON body
53 | * @param ServerRequestInterface $request
54 | * @return ServerRequestInterface
55 | */
56 | protected function parseJson(ServerRequestInterface $request): ServerRequestInterface
57 | {
58 | try {
59 | return $request->withParsedBody(json_decode((string)$request->getBody(), true, 512, JSON_THROW_ON_ERROR));
60 | } catch (JsonException $exception) {
61 | throw BadRequestException::createWithContext([], $exception);
62 | }
63 | }
64 |
65 | /**
66 | * Parse multipart/form-data body
67 | * @param ServerRequestInterface $request
68 | * @return ServerRequestInterface
69 | */
70 | protected function parseMultipart(ServerRequestInterface $request): ServerRequestInterface
71 | {
72 | return $request->withParsedBody(PSR7::convert($request));
73 | }
74 |
75 | /**
76 | * Parse application/x-www-form-urlencoded
77 | * @param ServerRequestInterface $request
78 | * @return ServerRequestInterface
79 | */
80 | protected function parseForm(ServerRequestInterface $request): ServerRequestInterface
81 | {
82 | $data = [];
83 | parse_str((string)$request->getBody(), $data);
84 | return $request->withParsedBody($data);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Bleeding/Routing/InvokeController.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Routing;
11 |
12 | use DI\Container;
13 | use JsonSerializable;
14 | use LogicException;
15 | use Psr\Http\Message\ServerRequestInterface;
16 | use Psr\Http\Message\ResponseFactoryInterface;
17 | use Psr\Http\Message\ResponseInterface;
18 | use Psr\Http\Server\RequestHandlerInterface;
19 | use Stringable;
20 |
21 | use function is_array;
22 | use function is_null;
23 | use function is_string;
24 | use function json_encode;
25 |
26 | /**
27 | * Main Userland Controller invoker
28 | * @package Bleeding\Routing
29 | */
30 | final class InvokeController implements RequestHandlerInterface
31 | {
32 | /**
33 | * @param Route $route
34 | * @param Container $container
35 | */
36 | public function __construct(
37 | private Route $route,
38 | private Container $container
39 | ) {}
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function handle(ServerRequestInterface $request): ResponseInterface
45 | {
46 | $route = $this->route;
47 |
48 | /** @var ResponseInterface|JsonSerializable|Stringable|array|string|null $result */
49 | $result = $this->container->call($this->route->getFunc(), compact('route', 'request'));
50 |
51 | if ($result instanceof ResponseInterface) {
52 | // Response has been created in controller
53 | return $result;
54 | }
55 |
56 | /** @var ResponseInterface $response */
57 | $response = $this->container->get(ResponseFactoryInterface::class)->createResponse(200);
58 |
59 | $this->writeResponse($response, $result);
60 |
61 | return $response
62 | ->withHeader('Content-Type', 'application/json; charset=utf-8')
63 | ->withHeader('Content-Length', (string)$response->getBody()->getSize());
64 | }
65 |
66 | /**
67 | * write to response body
68 | *
69 | * @param ResponseInterface $response
70 | * @param ResponseInterface|JsonSerializable|Stringable|array|string|null $result
71 | * @return void
72 | * @throws LogicException
73 | */
74 | private function writeResponse(ResponseInterface $response, mixed $result): void
75 | {
76 | match (true) {
77 | is_array($result), $result instanceof JsonSerializable =>
78 | $response->getBody()->write(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)),
79 | is_null($result), $result === '' =>
80 | $response->getBody()->write('{}'),
81 | is_string($result), $result instanceof Stringable =>
82 | /** @psalm-suppress PossiblyInvalidCast */
83 | $response->getBody()->write((string)$result),
84 | default => throw new LogicException(
85 | 'Controller response must be ResponseInterface|JsonSerializable|Stringable|array|string|null, got ' . get_debug_type($result),
86 | 500
87 | ),
88 | };
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Bleeding/Applications/ErrorHandler.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | use Psr\Log\LoggerInterface;
13 |
14 | use function array_map;
15 | use function compact;
16 | use function debug_backtrace;
17 | use function headers_sent;
18 | use function json_encode;
19 | use function restore_error_handler;
20 | use function set_error_handler;
21 | use function strpos;
22 |
23 | use const DEBUG_BACKTRACE_IGNORE_ARGS;
24 | use const JSON_UNESCAPED_UNICODE;
25 | use const PHP_SAPI;
26 |
27 | /**
28 | * catches PHP Errors and respond Error Response
29 | * @package Bleeding\Applications
30 | */
31 | final class ErrorHandler
32 | {
33 | /**
34 | * @param LoggerInterface $logger
35 | */
36 | public function __construct(
37 | private LoggerInterface $logger
38 | ) {}
39 |
40 | /**
41 | * Set global error handler
42 | * @return self
43 | */
44 | public function setErrorHandler(): self
45 | {
46 | set_error_handler([$this, 'handle']);
47 |
48 | return $this;
49 | }
50 |
51 | /**
52 | * Restore global error handler
53 | * @return void
54 | */
55 | public function restoreErrorHandler(): void
56 | {
57 | restore_error_handler();
58 | }
59 |
60 | /**
61 | * Set global error handler
62 | * @param int $errno
63 | * @param string $errstr
64 | * @param string $errfile
65 | * @param int $errline
66 | */
67 | public function handle(int $errno, string $errstr, string $errfile, int $errline): bool
68 | {
69 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);
70 | $trace = array_map(fn (array $arg) =>
71 | sprintf("%s%s%s() in %s:%s",
72 | $arg['class'] ?? '',
73 | $arg['type'] ?? '',
74 | $arg['function'] ?? '',
75 | $arg['file'] ?? '',
76 | $arg['line'] ?? ''
77 | ),
78 | $trace
79 | );
80 |
81 | $body = ['message' => $errstr, 'error' => compact('errno', 'errstr', 'errfile', 'errline', 'trace')];
82 | $bodyRaw = (strpos(PHP_SAPI, 'cli') !== false || getenv('DEBUG_MODE') === 'true') ? $body : ['message' => 'Internal Server Error'];
83 | $bodyString = json_encode($bodyRaw, JSON_UNESCAPED_UNICODE);
84 | $bodyLen = strlen($bodyString);
85 |
86 | if (error_reporting() === 0) {
87 | // through that error is suppressed by @
88 | $this->logger->debug($errstr, $body);
89 | return false;
90 | }
91 |
92 | $this->logger->error($errstr, $body);
93 |
94 | // respond HTTP
95 | if (strpos(PHP_SAPI, 'cli') === false && strpos(PHP_SAPI, 'phpdbg') === false && !headers_sent()) {
96 | // @codeCoverageIgnoreStart
97 | header('HTTP/1.1 500 Internal Server Error');
98 | header('content-type: application/json; charset=utf-8');
99 | header("content-length: ${bodyLen}");
100 | echo $bodyString;
101 | // @codeCoverageIgnoreEnd
102 | }
103 |
104 | return false;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Bleeding/Routing/RoutingResolver.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Routing;
11 |
12 | use Bleeding\Http\Exceptions\MethodNotAllowedException;
13 | use Bleeding\Http\Exceptions\NotFoundException;
14 | use DI\Container;
15 | use JsonSerializable;
16 | use Psr\Http\Message\ResponseFactoryInterface;
17 | use Psr\Http\Message\ResponseInterface;
18 | use Psr\Http\Message\ServerRequestInterface;
19 | use Psr\Http\Message\StreamFactoryInterface;
20 | use Psr\Http\Server\RequestHandlerInterface;
21 | use RecursiveDirectoryIterator;
22 | use RecursiveIteratorIterator;
23 | use ReflectionFunction;
24 | use Relay\RelayBuilder;
25 |
26 | use function array_key_exists;
27 | use function strtoupper;
28 | use function trim;
29 |
30 | /**
31 | * Resolve routing and invoke main controller
32 | * @package Bleeding\Routing
33 | */
34 | class RoutingResolver implements RequestHandlerInterface
35 | {
36 | /** @var string */
37 | protected string $baseDir;
38 |
39 | /** @var Container */
40 | protected Container $container;
41 |
42 | /**
43 | * @param string $baseDir base controller directory
44 | * @param Container $container IoC Container
45 | */
46 | public function __construct(string $baseDir, Container $container)
47 | {
48 | $this->baseDir = $baseDir;
49 | $this->container = $container;
50 | }
51 |
52 | /**
53 | * Resolve path
54 | *
55 | * @param ServerRequestInterface $request
56 | * @return ResponseInterface
57 | */
58 | public function handle(ServerRequestInterface $request): ResponseInterface
59 | {
60 | $path = '/' . trim($request->getUri()->getPath(), '/');
61 | $method = strtoupper($request->getMethod());
62 | $paths = CollectRoute::collect($this->baseDir);
63 |
64 | if (!array_key_exists($path, $paths)) {
65 | throw NotFoundException::createWithContext(compact('path', 'method'));
66 | }
67 |
68 | if (!array_key_exists($method, $paths[$path])) {
69 | // TODO: Support OPTIONS
70 | $allow = array_keys($paths[$path]);
71 | throw MethodNotAllowedException::createWithContext(compact('allow', 'path', 'method'));
72 | }
73 |
74 | $route = $paths[$path][$method];
75 |
76 | $queue = [];
77 | foreach ($route->getMiddlewares() as $middleware) {
78 | assert(class_exists($middleware));
79 | $queue[] = $middleware;
80 | }
81 |
82 | $container = $this->container;
83 |
84 | // Main controller invoke
85 | $queue[] = new InvokeController($route, $container);
86 |
87 | $relayBuilder = new RelayBuilder((new MiddlewareResolver($container))->createResolver());
88 | $response = $relayBuilder
89 | ->newInstance($queue)
90 | ->handle($request);
91 |
92 | if ($method === 'HEAD') {
93 | // fresh body
94 | /** @var StreamFactoryInterface $streamFactory */
95 | $streamFactory = $container->get(StreamFactoryInterface::class);
96 | return $response
97 | ->withBody($streamFactory->createStream(''));
98 | }
99 | return $response;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Bleeding/Applications/WebApplication.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | use Bleeding\Http\ServerRequestFactoryInterface;
13 | use Bleeding\Routing\MiddlewareResolver;
14 | use DI\Container;
15 | use Narrowspark\HttpEmitter\SapiEmitter;
16 | use Psr\Container\ContainerInterface;
17 | use Psr\Http\Message\ServerRequestInterface;
18 | use Psr\Http\Server\MiddlewareInterface;
19 | use Psr\Http\Server\RequestHandlerInterface;
20 | use Psr\Log\LoggerInterface;
21 | use Relay\RelayBuilder;
22 |
23 | use function assert;
24 | use function count;
25 | use function fastcgi_finish_request;
26 | use function function_exists;
27 | use function headers_sent;
28 |
29 | /**
30 | * @package Bleeding\Applications
31 | */
32 | abstract class WebApplication implements Application
33 | {
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function createLogger(): LoggerInterface
38 | {
39 | return LoggerFactory::create('Bleeding');
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function createContainer(): Container
46 | {
47 | return ContainerFactory::create();
48 | }
49 |
50 | /**
51 | * Creates middleware queue processes Request and Response
52 | *
53 | * @param Container $container
54 | * @return (MiddlewareInterface|RequestHandlerInterface|string)[]
55 | */
56 | abstract protected function createProcessQueue(Container $container): array;
57 |
58 | /**
59 | * Create ServerRequestInterface
60 | * @param ContainerInterface $container
61 | * @return ServerRequestInterface
62 | */
63 | protected function createServerRequest(ContainerInterface $container): ServerRequestInterface
64 | {
65 | /** @var ServerRequestFactoryInterface $factory */
66 | $factory = $container->get(ServerRequestFactoryInterface::class);
67 | return $factory->createFromGlobals();
68 | }
69 |
70 | /**
71 | * {@inheritdoc}
72 | */
73 | final public function run(): int
74 | {
75 | $logger = $this->createLogger();
76 | $errorHandler = (new ErrorHandler($logger));
77 | $errorHandler->setErrorHandler();
78 |
79 | $container = $this->createContainer();
80 | $container->set(LoggerInterface::class, $logger);
81 | $container->set(ContainerInterface::class, $container);
82 |
83 | $queue = $this->createProcessQueue($container);
84 | assert(0 < count($queue), 'queue has filled');
85 |
86 | $request = $this->createServerRequest($container);
87 | $response = (new RelayBuilder((new MiddlewareResolver($container))->createResolver()))
88 | ->newInstance($queue)
89 | ->handle($request);
90 |
91 | if (strpos(PHP_SAPI, 'cli') === false && strpos(PHP_SAPI, 'phpdbg') === false) {
92 | // @codeCoverageIgnoreStart
93 | (new SapiEmitter())->emit($response);
94 | assert(headers_sent());
95 |
96 | if (function_exists('fastcgi_finish_request')) {
97 | fastcgi_finish_request();
98 | }
99 | // @codeCoverageIgnoreEnd
100 | }
101 |
102 | $errorHandler->restoreErrorHandler();
103 | return 0;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/MiddlewareResolverTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 | use Bleeding\Routing\MiddlewareResolver;
14 | use LogicException;
15 | use Psr\Container\ContainerInterface;
16 | use Psr\Http\Server\RequestHandlerInterface;
17 | use Psr\Http\Server\MiddlewareInterface;
18 | use Tests\TestCase;
19 |
20 | /**
21 | * @package Tests\Bleeding\Routing
22 | * @immutable
23 | * @coversDefaultClass \Bleeding\Routing\MiddlewareResolver
24 | */
25 | final class MiddlewareResolverTest extends TestCase
26 | {
27 | /**
28 | * @test
29 | * @covers ::__construct
30 | * @covers ::createResolver
31 | * @covers ::isValidInstance
32 | */
33 | public function testClassCallable(): void
34 | {
35 | $container = new class implements ContainerInterface {
36 | public function get($id) {}
37 | public function has($id) {}
38 | };
39 |
40 | $resolver = (new MiddlewareResolver($container))->createResolver();
41 | $entry = fn () => true;
42 |
43 | $actual = $resolver($entry);
44 |
45 | $this->assertSame($entry, $actual);
46 | }
47 |
48 | /**
49 | * @test
50 | * @covers ::__construct
51 | * @covers ::createResolver
52 | * @covers ::isValidInstance
53 | * @uses \Bleeding\Exceptions\RuntimeException
54 | */
55 | public function testClassNotFound(): void
56 | {
57 | $container = new class implements ContainerInterface {
58 | public function get($id) {}
59 | public function has($id) {}
60 | };
61 |
62 | $resolver = new MiddlewareResolver($container);
63 |
64 | $this->expectException(RuntimeException::class);
65 | $this->expectExceptionCode(1);
66 | $this->expectExceptionMessage('Cannot resolve Middleware entry: UnknownKlass');
67 |
68 | $resolver = $resolver->createResolver();
69 | $resolver('UnknownKlass');
70 | }
71 |
72 | /**
73 | * @test
74 | * @covers ::__construct
75 | * @covers ::createResolver
76 | * @covers ::isValidInstance
77 | */
78 | public function testClassHasNoEntry(): void
79 | {
80 | $container = new class implements ContainerInterface {
81 | public function get($id) {}
82 | public function has($id) { return false; }
83 | };
84 |
85 | $resolver = (new MiddlewareResolver($container))->createResolver();
86 |
87 | $this->expectException(LogicException::class);
88 | $this->expectExceptionCode(0);
89 | $this->expectExceptionMessage('Unknown Middleware in Container: Tests\\Bleeding\\Routing\\MiddlewareResolverTest');
90 |
91 | $resolver(self::class);
92 | }
93 |
94 | /**
95 | * @test
96 | * @covers ::__construct
97 | * @covers ::createResolver
98 | * @covers ::isValidInstance
99 | */
100 | public function testClassGetFromContainer(): void
101 | {
102 | $container = new class implements ContainerInterface {
103 | public function get($id) { return fn() => true; }
104 | public function has($id) { return true; }
105 | };
106 |
107 | $resolver = (new MiddlewareResolver($container))->createResolver();
108 | $entry = $resolver(self::class);
109 |
110 | $this->assertTrue(is_callable($entry));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/Bleeding/Applications/ConsoleApplication.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Applications;
11 |
12 | use Bleeding\Console\CollectCommand;
13 | use DI\Container;
14 | use Psr\Container\ContainerInterface;
15 | use Psr\Log\LoggerInterface;
16 | use RecursiveDirectoryIterator;
17 | use RecursiveIteratorIterator;
18 | use Silly\Application as ApplicationBase;
19 | use Symfony\Component\Console\Command\Command;
20 | use Symfony\Component\Console\Input\ArgvInput;
21 | use Symfony\Component\Console\Input\InputInterface;
22 | use Symfony\Component\Console\Logger\ConsoleLogger;
23 | use Symfony\Component\Console\Output\ConsoleOutput;
24 | use Symfony\Component\Console\Output\OutputInterface;
25 | use Throwable;
26 |
27 | use function file_exists;
28 |
29 | /**
30 | * @package Bleeding\Applications
31 | */
32 | abstract class ConsoleApplication implements Application
33 | {
34 | /**
35 | * @var InputInterface $input
36 | * @readonly
37 | */
38 | protected InputInterface $input;
39 |
40 | /**
41 | * @var OutputInterface $output
42 | * @readonly
43 | */
44 | protected OutputInterface $output;
45 |
46 | /**
47 | * @param ?InputInterface $input
48 | * @param ?OutputInterface $output
49 | */
50 | public function __construct(
51 | ?InputInterface $input = null,
52 | ?OutputInterface $output = null
53 | ) {
54 | $this->input = $input ?? new ArgvInput();
55 | $this->output = $output ?? new ConsoleOutput();
56 | }
57 |
58 | /**
59 | * create logger
60 | * @return LoggerInterface
61 | */
62 | public function createLogger(): LoggerInterface
63 | {
64 | return new ConsoleLogger($this->output);
65 | }
66 |
67 | /**
68 | * create IoC container
69 | * @return Container
70 | */
71 | public function createContainer(): Container
72 | {
73 | return ContainerFactory::create();
74 | }
75 |
76 | /**
77 | * Get parent directory for commands
78 | * @return string
79 | */
80 | abstract protected function getCommandDirectory(): string;
81 |
82 | /**
83 | * {@inheritdoc}
84 | */
85 | final public function run(): int
86 | {
87 | $logger = $this->createLogger();
88 | $errorHandler = (new ErrorHandler($logger));
89 | $errorHandler->setErrorHandler();
90 |
91 | $container = $this->createContainer();
92 | $container->set(LoggerInterface::class, $logger);
93 | $container->set(ContainerInterface::class, $container);
94 | $container->set(InputInterface::class, $this->input);
95 | $container->set(OutputInterface::class, $this->output);
96 |
97 | $app = new ApplicationBase(static::APP_NAME, static::APP_VERSION);
98 | $app->useContainer($container, true, true);
99 | $app->setAutoExit(false);
100 |
101 | assert(file_exists($this->getCommandDirectory()));
102 | $commands = CollectCommand::collect($this->getCommandDirectory());
103 | foreach ($commands as $command) {
104 | $app->command($command->getDefinition(), $command->getFunc());
105 | }
106 |
107 | try {
108 | return $app->run($this->input, $this->output);
109 | // @codeCoverageIgnoreStart
110 | } finally {
111 | $errorHandler->restoreErrorHandler();
112 | }
113 |
114 | return 1;
115 | // @codeCoverageIgnoreEnd
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Bleeding/Http/Middlewares/ProcessErrorMiddleware.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Http\Middlewares;
11 |
12 | use Bleeding\Exceptions\RuntimeException;
13 | use Bleeding\Http\Exceptions\MethodNotAllowedException;
14 | use Psr\Http\Message\ServerRequestInterface;
15 | use Psr\Http\Message\ResponseFactoryInterface;
16 | use Psr\Http\Message\ResponseInterface;
17 | use Psr\Http\Server\MiddlewareInterface;
18 | use Psr\Http\Server\RequestHandlerInterface;
19 | use Psr\Log\LoggerInterface;
20 | use Throwable;
21 |
22 | use function array_map;
23 | use function getenv;
24 | use function get_debug_type;
25 | use function implode;
26 | use function json_encode;
27 | use function strlen;
28 |
29 | use const JSON_THROW_ON_ERROR;
30 | use const JSON_UNESCAPED_UNICODE;
31 |
32 | /**
33 | * Process Runtime Error
34 | * @package Bleeding\Http\Middlewares
35 | */
36 | final class ProcessErrorMiddleware implements MiddlewareInterface
37 | {
38 | /**
39 | * constructor
40 | *
41 | * @param ResponseFactoryInterface $responseFactory
42 | * @param LoggerInterface $logger
43 | */
44 | public function __construct(
45 | private ResponseFactoryInterface $responseFactory,
46 | private LoggerInterface $logger
47 | ) {}
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
53 | {
54 | try {
55 | return $handler->handle($request);
56 | } catch (Throwable $throwable) {
57 | return $this->processThrowable($request, $throwable);
58 | }
59 | }
60 |
61 | /**
62 | * @param ServerRequestInterface $request
63 | * @param Throwable $throwable
64 | * @return ResponseInterface
65 | */
66 | private function processThrowable(ServerRequestInterface $request, Throwable $throwable): ResponseInterface
67 | {
68 | $code = $throwable->getCode();
69 | $response = $this->responseFactory->createResponse(($code > 100 && $code < 600) ? (int)$code : 500);
70 | $backtrace = array_map(fn (array $arg) =>
71 | sprintf("%s%s%s() in %s:%s",
72 | $arg['class'] ?? '',
73 | $arg['type'] ?? '',
74 | $arg['function'] ?? '',
75 | $arg['file'] ?? '',
76 | $arg['line'] ?? ''
77 | ),
78 | $throwable->getTrace()
79 | );
80 | $body = [
81 | 'type' => get_debug_type($throwable),
82 | 'message' => $throwable->getMessage(),
83 | 'code' => $throwable->getCode(),
84 | 'file' => $throwable->getFile(),
85 | 'line' => $throwable->getLine(),
86 | 'context' => $throwable instanceof RuntimeException ? $throwable->getContext() : [],
87 | 'previous' => $throwable->getPrevious(),
88 | 'trace' => $backtrace,
89 | ];
90 |
91 | $this->logger->error($throwable->getMessage(), $body);
92 | if (getenv('DEBUG_MODE') !== 'true') {
93 | $body = ['message' => 'Internal Server Error'];
94 | }
95 | $bodyRaw = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
96 | $response->getBody()->write($bodyRaw);
97 | if ($throwable instanceof MethodNotAllowedException) {
98 | $response = $response->withHeader('Allow', implode(',', $throwable->getContext()['allow']));
99 | }
100 | return $response->withHeader('Content-Type', 'application/json;charset=utf-8')
101 | ->withHeader('Content-Length', (string)strlen($bodyRaw));
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Bleeding/Routing/CollectRoute.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Bleeding\Routing;
11 |
12 | use Bleeding\Http\Attributes\Middleware;
13 | use Bleeding\Routing\Attributes\Get;
14 | use Bleeding\Routing\Attributes\Post;
15 | use LogicException;
16 | use RecursiveDirectoryIterator;
17 | use RecursiveIteratorIterator;
18 | use ReflectionFunction;
19 | use SplFileInfo;
20 |
21 | use function array_reduce;
22 | use function is_callable;
23 | use function is_null;
24 | use function str_ends_with;
25 | use function trim;
26 |
27 | /**
28 | * @package Bleeding\Routing
29 | * @immutable
30 | */
31 | final class CollectRoute
32 | {
33 | /**
34 | * List up all routes
35 | *
36 | * @todo PHP file caching
37 | * @return array
38 | */
39 | public static function collect(string $baseDir): array
40 | {
41 | // TODO: Collect from cache
42 | // TODO: multiple baseDir
43 | $iterator = new RecursiveIteratorIterator(
44 | new RecursiveDirectoryIterator($baseDir),
45 | RecursiveIteratorIterator::SELF_FIRST
46 | );
47 |
48 | $paths = [];
49 |
50 | foreach ($iterator as $file) {
51 | $route = self::checkFile($file);
52 |
53 | if (!is_null($route)) {
54 | assert(!isset($paths[$route->getPath()][$route->getMethod()]), 'path is not conflicted');
55 | $paths[$route->getPath()][$route->getMethod()] = $route;
56 | if ($route->getMethod() === 'GET') {
57 | // Add HEAD routing
58 | $paths[$route->getPath()]['HEAD'] = $route;
59 | }
60 | }
61 | }
62 |
63 | return $paths;
64 | }
65 |
66 | /**
67 | * @internal
68 | * @param SplFileInfo $file
69 | * @return ?Route
70 | */
71 | private static function checkFile(SplFileInfo $file): ?Route
72 | {
73 | if (!str_ends_with($file->getBaseName(), '.php')) {
74 | return null;
75 | }
76 |
77 | $func = require $file->getRealPath();
78 | assert(is_callable($func), "controller {$file->getRealPath()} is callable");
79 |
80 | /** @psalm-suppress InvalidArgument */
81 | $ref = new ReflectionFunction($func);
82 | assert(0 < count($ref->getAttributes()), "Controller {$file->getRealPath()} has attribute");
83 |
84 | $middlewares = self::getMiddlewares($ref);
85 | $attr = self::getAttribute($ref);
86 |
87 | return new Route(
88 | $attr->getPath(),
89 | $attr->getMethodName(),
90 | $func,
91 | $file->getRealPath(),
92 | $middlewares
93 | );
94 | }
95 |
96 | /**
97 | * @param ReflectionFunction $ref
98 | * @return string[]
99 | */
100 | private static function getMiddlewares(ReflectionFunction $ref): array
101 | {
102 | $middlewares = [];
103 | if (0 === count($ref->getAttributes(Middleware::class))) {
104 | return $middlewares;
105 | }
106 | $attr = $ref->getAttributes(Middleware::class)[0]->newInstance();
107 | $middlewares = $attr->getMiddlewareNames();
108 | assert(
109 | array_reduce(
110 | $middlewares,
111 | fn (bool $carry, string $item) => ($carry && class_exists($item)),
112 | true
113 | ),
114 | 'All middleware class exists'
115 | );
116 |
117 | return $middlewares;
118 | }
119 |
120 | /**
121 | * Get Get|Post Attribute
122 | * @param ReflectionFunction $ref
123 | * @return Get|Post
124 | */
125 | private static function getAttribute(ReflectionFunction $ref): Get|Post
126 | {
127 | $attr = $ref->getAttributes(Get::class);
128 | if (1 === count($attr)) {
129 | return $attr[0]->newInstance();
130 | }
131 | $attr = $ref->getAttributes(Post::class);
132 | if (1 === count($attr)) {
133 | return $attr[0]->newInstance();
134 | }
135 | throw new LogicException('Unknown routing funciton found: ' . $ref->getFileName()); // @codeCoverageIgnore
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/InvokeControllerTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing;
11 |
12 | use DI\Container;
13 | use Bleeding\Exceptions\RuntimeException;
14 | use Bleeding\Routing\InvokeController;
15 | use Bleeding\Routing\Route;
16 | use JsonSerializable;
17 | use Laminas\Diactoros\Response;
18 | use Laminas\Diactoros\ResponseFactory;
19 | use Laminas\Diactoros\ServerRequest;
20 | use Psr\Http\Message\ResponseFactoryInterface;
21 | use Psr\Http\Server\RequestHandlerInterface;
22 | use Stringable;
23 | use Tests\TestCase;
24 |
25 | /**
26 | * @package Tests\Bleeding\Routing
27 | * @immutable
28 | * @coversDefaultClass \Bleeding\Routing\InvokeController
29 | */
30 | final class InvokeControllerTest extends TestCase
31 | {
32 | /**
33 | * @test
34 | * @covers ::__construct
35 | * @covers ::handle
36 | * @uses \Bleeding\Routing\Route
37 | */
38 | public function testRawResponse(): void
39 | {
40 | $response = new Response();
41 | $route = new Route('/', 'GET', fn ($request, $route) => $response, '', []);
42 | $container = new Container();
43 | $request = new ServerRequest();
44 | $invoke = new InvokeController($route, $container);
45 |
46 | $actual = $invoke->handle($request);
47 |
48 | $this->assertSame($response, $actual);
49 | }
50 |
51 | /**
52 | * Main Data provider
53 | */
54 | public function mainDataProvider(): array
55 | {
56 | return [
57 | 'array' => [
58 | ['Hello' => 'world'],
59 | '{"Hello":"world"}',
60 | ],
61 | 'JsonSerializable' => [
62 | new class implements JsonSerializable {
63 | public function jsonSerialize(): array { return ['Hello' => 'world2']; }
64 | },
65 | '{"Hello":"world2"}',
66 | ],
67 | 'null' => [
68 | null,
69 | '{}',
70 | ],
71 | 'empty string' => [
72 | '',
73 | '{}',
74 | ],
75 | 'string' => [
76 | '[]',
77 | '[]',
78 | ],
79 | 'Stringable' => [
80 | new class implements Stringable {
81 | public function __toString(): string { return '{"Hello":"world3"}'; }
82 | },
83 | '{"Hello":"world3"}',
84 | ]
85 | ];
86 | }
87 |
88 | /**
89 | * @test
90 | * @covers ::__construct
91 | * @covers ::handle
92 | * @covers ::writeResponse
93 | * @uses \Bleeding\Routing\Route
94 | * @dataProvider mainDataProvider
95 | */
96 | public function testWriteResponse(mixed $output, string $expected): void
97 | {
98 | $response = new Response();
99 | $route = new Route('/', 'GET', fn ($request, $route) => $output, '', []);
100 | $container = new Container();
101 | $container->set(ResponseFactoryInterface::class, new ResponseFactory);
102 | $request = new ServerRequest();
103 | $invoke = new InvokeController($route, $container);
104 |
105 | $actual = $invoke->handle($request);
106 |
107 | $this->assertSame($expected, (string)$actual->getBody());
108 | $this->assertSame('application/json; charset=utf-8', $actual->getHeaderLine('Content-Type'));
109 | $this->assertSame((int)strlen($expected), (int)$actual->getBody()->getSize());
110 | }
111 |
112 | /**
113 | * @test
114 | * @covers ::__construct
115 | * @covers ::handle
116 | * @covers ::writeResponse
117 | * @uses \Bleeding\Routing\Route
118 | */
119 | public function testUnknownResponse(): void
120 | {
121 | $response = new Response();
122 | $route = new Route('/', 'GET', fn ($request, $route) => 0, '', []);
123 | $container = new Container();
124 | $container->set(ResponseFactoryInterface::class, new ResponseFactory);
125 | $request = new ServerRequest();
126 | $invoke = new InvokeController($route, $container);
127 |
128 | $this->expectException(\LogicException::class);
129 | $this->expectExceptionMessage('Controller response must be ResponseInterface|JsonSerializable|Stringable|array|string|null, got int');
130 | $invoke->handle($request);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/Bleeding/Http/Middlewares/ProcessErrorMiddlewareTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Http\Middlewares;
11 |
12 | use Bleeding\Http\Exceptions\MethodNotAllowedException;
13 | use Bleeding\Http\Middlewares\ProcessErrorMiddleware;
14 | use Laminas\Diactoros\Response;
15 | use Laminas\Diactoros\ResponseFactory;
16 | use Laminas\Diactoros\ServerRequest;
17 | use Laminas\Diactoros\Stream;
18 | use Monolog\Handler\TestHandler;
19 | use Monolog\Logger;
20 | use Psr\Http\Message\ServerRequestInterface;
21 | use Psr\Http\Message\ResponseInterface;
22 | use Psr\Http\Server\MiddlewareInterface;
23 | use Psr\Http\Server\RequestHandlerInterface;
24 | use Tests\TestCase;
25 |
26 | /**
27 | * @package Tests\Bleeding\Http\Middlewares
28 | * @coversDefaultClass \Bleeding\Http\Middlewares\ProcessErrorMiddleware
29 | * @immutable
30 | */
31 | final class ProcessErrorMiddlewareTest extends TestCase
32 | {
33 | /**
34 | * @test
35 | * @covers ::__construct
36 | * @covers ::process
37 | * @covers ::processThrowable
38 | */
39 | public function testProcessThrowable(): void
40 | {
41 | $request = new ServerRequest(
42 | [],
43 | [],
44 | '/',
45 | 'GET',
46 | new Stream('php://temp', 'rw'),
47 | ['Content-Type' => 'application/json']
48 | );
49 | $handler = new class implements RequestHandlerInterface {
50 | public function handle(ServerRequestInterface $request): ResponseInterface
51 | {
52 | throw new \Exception('Error');
53 | }
54 | };
55 | $logger = new Logger('Bleeding Test');
56 | $loggerHandler = new TestHandler();
57 | $logger->pushHandler($loggerHandler);
58 | $middleware = new ProcessErrorMiddleware(new ResponseFactory(), $logger);
59 |
60 | $response = $middleware->process($request, $handler);
61 |
62 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
63 | $this->assertInstanceOf(ResponseInterface::class, $response);
64 | $this->assertSame(500, $response->getStatusCode());
65 | $this->assertTrue($loggerHandler->hasErrorThatContains('Error'));
66 | }
67 |
68 | /**
69 | * @test
70 | * @covers ::__construct
71 | * @covers ::process
72 | * @covers ::processThrowable
73 | */
74 | public function testProcessThrowableOnNoDebugMode(): void
75 | {
76 | // no debug mode
77 | putenv('DEBUG_MODE=false');
78 |
79 | $request = new ServerRequest(
80 | [],
81 | [],
82 | '/',
83 | 'GET',
84 | new Stream('php://temp', 'rw'),
85 | ['Content-Type' => 'application/json']
86 | );
87 | $handler = new class implements RequestHandlerInterface {
88 | public function handle(ServerRequestInterface $request): ResponseInterface
89 | {
90 | throw new \Exception('Error', 503);
91 | }
92 | };
93 | $logger = new Logger('Bleeding Test');
94 | $loggerHandler = new TestHandler();
95 | $logger->pushHandler($loggerHandler);
96 | $middleware = new ProcessErrorMiddleware(new ResponseFactory(), $logger);
97 |
98 | $response = $middleware->process($request, $handler);
99 |
100 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
101 | $this->assertInstanceOf(ResponseInterface::class, $response);
102 | $this->assertSame(503, $response->getStatusCode());
103 | $this->assertSame('{"message":"Internal Server Error"}', (string)$response->getBody());
104 | $this->assertTrue($loggerHandler->hasErrorThatContains('Error'));
105 | }
106 |
107 | /**
108 | * @test
109 | * @covers ::__construct
110 | * @covers ::process
111 | * @covers ::processThrowable
112 | * @uses \Bleeding\Exceptions\RuntimeException
113 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
114 | */
115 | public function testProcessThrowableOnMethodNotAllowedException(): void
116 | {
117 | $request = new ServerRequest(
118 | [],
119 | [],
120 | '/',
121 | 'GET',
122 | new Stream('php://temp', 'rw'),
123 | ['Content-Type' => 'application/json']
124 | );
125 | $handler = new class implements RequestHandlerInterface {
126 | public function handle(ServerRequestInterface $request): ResponseInterface
127 | {
128 | throw MethodNotAllowedException::createWithContext(['allow' => ['GET', 'POST']]);
129 | }
130 | };
131 | $logger = new Logger('Bleeding Test');
132 | $loggerHandler = new TestHandler();
133 | $logger->pushHandler($loggerHandler);
134 | $middleware = new ProcessErrorMiddleware(new ResponseFactory(), $logger);
135 |
136 | $response = $middleware->process($request, $handler);
137 |
138 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
139 | $this->assertInstanceOf(ResponseInterface::class, $response);
140 | $this->assertSame(405, $response->getStatusCode());
141 | $this->assertSame('GET,POST', $response->getHeaderLine('Allow'));
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/tests/Bleeding/Routing/RoutingResolverTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Routing;
11 | use Bleeding\Http\Exceptions\MethodNotAllowedException;
12 | use Bleeding\Http\Exceptions\NotFoundException;
13 | use Bleeding\Routing\RoutingResolver;
14 | use DI\Container;
15 | use Laminas\Diactoros\ResponseFactory;
16 | use Laminas\Diactoros\ServerRequest;
17 | use Laminas\Diactoros\StreamFactory;
18 | use Tests\TestCase;
19 | use Psr\Http\Message\ResponseFactoryInterface;
20 | use Psr\Http\Message\StreamFactoryInterface;
21 |
22 | /**
23 | * @package Tests\Bleeding\Routing
24 | * @coversDefaultClass \Bleeding\Routing\RoutingResolver
25 | * @immutable
26 | */
27 | final class RoutingResolverTest extends TestCase
28 | {
29 | /**
30 | * @test
31 | * @covers ::__construct
32 | * @uses \Bleeding\Exceptions\RuntimeException
33 | * @uses \Bleeding\Http\Attributes\Middleware
34 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
35 | * @uses \Bleeding\Routing\Attributes\Get
36 | * @uses \Bleeding\Routing\Attributes\Post
37 | * @uses \Bleeding\Routing\CollectRoute
38 | * @uses \Bleeding\Routing\Route
39 | */
40 | public function testConstruct(): void
41 | {
42 | $container = new Container();
43 | $resolver = new RoutingResolver(__DIR__, $container);
44 |
45 | $this->assertInstanceOf(RoutingResolver::class, $resolver);
46 | }
47 |
48 | /**
49 | * @test
50 | * @covers ::__construct
51 | * @covers ::handle
52 | * @uses \Bleeding\Exceptions\RuntimeException
53 | * @uses \Bleeding\Http\Attributes\Middleware
54 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
55 | * @uses \Bleeding\Routing\Attributes\Get
56 | * @uses \Bleeding\Routing\Attributes\Post
57 | * @uses \Bleeding\Routing\CollectRoute
58 | * @uses \Bleeding\Routing\Route
59 | */
60 | public function testNotFound(): void
61 | {
62 | $container = new Container();
63 | $resolver = new RoutingResolver(__DIR__ . '/../../Stub/Routing', $container);
64 |
65 | $this->expectException(NotFoundException::class);
66 | $resolver->handle(new ServerRequest([], [], '/unknown-path', 'GET'));
67 | }
68 |
69 | /**
70 | * @test
71 | * @covers ::__construct
72 | * @covers ::handle
73 | * @uses \Bleeding\Exceptions\RuntimeException
74 | * @uses \Bleeding\Http\Attributes\Middleware
75 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
76 | * @uses \Bleeding\Routing\Attributes\Get
77 | * @uses \Bleeding\Routing\Attributes\Post
78 | * @uses \Bleeding\Routing\CollectRoute
79 | * @uses \Bleeding\Routing\Route
80 | */
81 | public function testMethodNotAllowed(): void
82 | {
83 | $container = new Container();
84 | $resolver = new RoutingResolver(__DIR__ . '/../../Stub/Routing', $container);
85 |
86 | $this->expectException(MethodNotAllowedException::class);
87 | $resolver->handle(new ServerRequest([], [], '/', 'PUT'));
88 | }
89 |
90 | /**
91 | * @test
92 | * @covers ::__construct
93 | * @covers ::handle
94 | * @uses \Bleeding\Exceptions\RuntimeException
95 | * @uses \Bleeding\Http\Attributes\Middleware
96 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
97 | * @uses \Bleeding\Routing\Attributes\Get
98 | * @uses \Bleeding\Routing\Attributes\Post
99 | * @uses \Bleeding\Routing\CollectRoute
100 | * @uses \Bleeding\Routing\InvokeController
101 | * @uses \Bleeding\Routing\MiddlewareResolver
102 | * @uses \Bleeding\Routing\Route
103 | */
104 | public function testHead(): void
105 | {
106 | $container = new Container();
107 | $container->set(ResponseFactoryInterface::class, new ResponseFactory);
108 | $container->set(StreamFactoryInterface::class, new StreamFactory);
109 | $resolver = new RoutingResolver(__DIR__ . '/../../Stub/Routing', $container);
110 |
111 | $response = $resolver->handle(new ServerRequest([], [], '/middleware', 'HEAD'));
112 |
113 | $this->assertSame('', (string)$response->getBody());
114 | }
115 |
116 | /**
117 | * @test
118 | * @covers ::__construct
119 | * @covers ::handle
120 | * @uses \Bleeding\Exceptions\RuntimeException
121 | * @uses \Bleeding\Http\Attributes\Middleware
122 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
123 | * @uses \Bleeding\Routing\Attributes\Get
124 | * @uses \Bleeding\Routing\Attributes\Post
125 | * @uses \Bleeding\Routing\CollectRoute
126 | * @uses \Bleeding\Routing\InvokeController
127 | * @uses \Bleeding\Routing\MiddlewareResolver
128 | * @uses \Bleeding\Routing\Route
129 | */
130 | public function testGet(): void
131 | {
132 | $container = new Container();
133 | $container->set(ResponseFactoryInterface::class, new ResponseFactory);
134 | $container->set(StreamFactoryInterface::class, new StreamFactory);
135 | $resolver = new RoutingResolver(__DIR__ . '/../../Stub/Routing', $container);
136 |
137 | $response = $resolver->handle(new ServerRequest([], [], '/', 'GET'));
138 |
139 | $this->assertSame('{"Hello":"world"}', (string)$response->getBody());
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/tests/Bleeding/Http/Middlewares/ParseBodyMiddlewareTest.php:
--------------------------------------------------------------------------------
1 |
5 | * @copyright 2020- Masaru Yamagishi
6 | */
7 |
8 | declare(strict_types=1);
9 |
10 | namespace Tests\Bleeding\Http\Middlewares;
11 |
12 | use Bleeding\Http\Exceptions\BadRequestException;
13 | use Bleeding\Http\Middlewares\ParseBodyMiddleware;
14 | use Laminas\Diactoros\Response;
15 | use Laminas\Diactoros\ServerRequest;
16 | use Laminas\Diactoros\Stream;
17 | use Psr\Http\Message\ServerRequestInterface;
18 | use Psr\Http\Message\ResponseInterface;
19 | use Psr\Http\Server\MiddlewareInterface;
20 | use Psr\Http\Server\RequestHandlerInterface;
21 | use Riverline\MultiPartParser\StreamedPart;
22 | use Tests\TestCase;
23 |
24 | /**
25 | * @package Tests\Bleeding\Http\Middlewares
26 | * @coversDefaultClass \Bleeding\Http\Middlewares\ParseBodyMiddleware
27 | * @immutable
28 | */
29 | final class ParseBodyMiddlewareTest extends TestCase
30 | {
31 | public function createRequestHandler($expected): RequestHandlerInterface
32 | {
33 | return new class ($expected) implements RequestHandlerInterface {
34 | public $expected;
35 | public function __construct($expected)
36 | {
37 | $this->expected = $expected;
38 | }
39 | public function handle(ServerRequestInterface $request): ResponseInterface
40 | {
41 | if ($request->getParsedBody() instanceof StreamedPart) {
42 | assert($request->getParsedBody()->isMultipart());
43 | } else {
44 | assert($request->getParsedBody() === $this->expected);
45 | }
46 | return new Response();
47 | }
48 | };
49 | }
50 |
51 | /**
52 | * @test
53 | * @covers ::process
54 | * @covers ::parseJson
55 | */
56 | public function testParseJson(): void
57 | {
58 | $request = new ServerRequest(
59 | [],
60 | [],
61 | '/',
62 | 'GET',
63 | new Stream('php://temp', 'rw'),
64 | ['Content-Type' => 'application/json']
65 | );
66 | $request->getBody()->write('{"Hello":"world"}');
67 | $handler = $this->createRequestHandler(['Hello' => 'world']);
68 | $middleware = new ParseBodyMiddleware();
69 |
70 | $response = $middleware->process($request, $handler);
71 |
72 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
73 | $this->assertInstanceOf(ResponseInterface::class, $response);
74 | }
75 |
76 | /**
77 | * @test
78 | * @covers ::process
79 | * @covers ::parseJson
80 | * @uses \Bleeding\Exceptions\RuntimeException
81 | * @uses \Bleeding\Http\Exceptions\InternalServerErrorException
82 | */
83 | public function testParseJsonFailed(): void
84 | {
85 | $request = new ServerRequest(
86 | [],
87 | [],
88 | '/',
89 | 'GET',
90 | new Stream('php://temp', 'rw'),
91 | ['Content-Type' => 'application/json']
92 | );
93 | $request->getBody()->write('this is not json');
94 | $handler = $this->createRequestHandler(null);
95 | $middleware = new ParseBodyMiddleware();
96 |
97 | $this->expectException(BadRequestException::class);
98 |
99 | $middleware->process($request, $handler);
100 | }
101 |
102 | /**
103 | * @see https://github.com/Riverline/multipart-parser/blob/master/tests/Converters/PSR7Test.php
104 | */
105 | public function createBodyStream()
106 | {
107 | $content = <<createBodyStream(),
148 | ['Content-type' => 'multipart/form-data; boundary=----------------------------83ff53821b7c']
149 | );
150 | $handler = $this->createRequestHandler(['foo' => 'bar']);
151 | $middleware = new ParseBodyMiddleware();
152 |
153 | $response = $middleware->process($request, $handler);
154 |
155 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
156 | $this->assertInstanceOf(ResponseInterface::class, $response);
157 | }
158 |
159 | /**
160 | * @test
161 | * @covers ::process
162 | * @covers ::parseForm
163 | */
164 | public function testParseForm(): void
165 | {
166 |
167 | $request = new ServerRequest(
168 | [],
169 | [],
170 | '/',
171 | 'GET',
172 | new Stream('php://temp', 'rw'),
173 | ['Content-type' => 'application/x-www-form-urlencoded']
174 | );
175 | $request->getBody()->write('first=value&bar=baz');
176 | $handler = $this->createRequestHandler(['first' => 'value', 'bar' => 'baz']);
177 | $middleware = new ParseBodyMiddleware();
178 |
179 | $response = $middleware->process($request, $handler);
180 |
181 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
182 | $this->assertInstanceOf(ResponseInterface::class, $response);
183 | }
184 |
185 | /**
186 | * @test
187 | * @covers ::process
188 | */
189 | public function testParseDefault(): void
190 | {
191 | $request = new ServerRequest(
192 | [],
193 | [],
194 | '/',
195 | 'GET',
196 | new Stream('php://temp', 'rw'),
197 | ['Content-Type' => 'text/plain']
198 | );
199 | $handler = $this->createRequestHandler(null);
200 | $middleware = new ParseBodyMiddleware();
201 |
202 | $response = $middleware->process($request, $handler);
203 |
204 | $this->assertInstanceOf(MiddlewareInterface::class, $middleware);
205 | $this->assertInstanceOf(ResponseInterface::class, $response);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Masaru Yamagishi
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/container/web/www.conf:
--------------------------------------------------------------------------------
1 | ; Start a new pool named 'www'.
2 | ; the variable $pool can be used in any directive and will be replaced by the
3 | ; pool name ('www' here)
4 | [www]
5 |
6 | ; Per pool prefix
7 | ; It only applies on the following directives:
8 | ; - 'access.log'
9 | ; - 'slowlog'
10 | ; - 'listen' (unixsocket)
11 | ; - 'chroot'
12 | ; - 'chdir'
13 | ; - 'php_values'
14 | ; - 'php_admin_values'
15 | ; When not set, the global prefix (or @php_fpm_prefix@) applies instead.
16 | ; Note: This directive can also be relative to the global prefix.
17 | ; Default Value: none
18 | ;prefix = /path/to/pools/$pool
19 |
20 | ; Unix user/group of processes
21 | ; Note: The user is mandatory. If the group is not set, the default user's group
22 | ; will be used.
23 | user = www-data
24 | group = www-data
25 |
26 | ; The address on which to accept FastCGI requests.
27 | ; Valid syntaxes are:
28 | ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
29 | ; a specific port;
30 | ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
31 | ; a specific port;
32 | ; 'port' - to listen on a TCP socket to all addresses
33 | ; (IPv6 and IPv4-mapped) on a specific port;
34 | ; '/path/to/unix/socket' - to listen on a unix socket.
35 | ; Note: This value is mandatory.
36 | listen = /var/run/php-fpm/www.sock
37 |
38 | ; Set listen(2) backlog.
39 | ; Default Value: 511 (-1 on FreeBSD and OpenBSD)
40 | ;listen.backlog = 511
41 |
42 | ; Set permissions for unix socket, if one is used. In Linux, read/write
43 | ; permissions must be set in order to allow connections from a web server. Many
44 | ; BSD-derived systems allow connections regardless of permissions. The owner
45 | ; and group can be specified either by name or by their numeric IDs.
46 | ; Default Values: user and group are set as the running user
47 | ; mode is set to 0660
48 | listen.owner = www-data
49 | listen.group = www-data
50 | listen.mode = 0666
51 | ; When POSIX Access Control Lists are supported you can set them using
52 | ; these options, value is a comma separated list of user/group names.
53 | ; When set, listen.owner and listen.group are ignored
54 | ;listen.acl_users =
55 | ;listen.acl_groups =
56 |
57 | ; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
58 | ; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
59 | ; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
60 | ; must be separated by a comma. If this value is left blank, connections will be
61 | ; accepted from any ip address.
62 | ; Default Value: any
63 | ;listen.allowed_clients = 127.0.0.1
64 |
65 | ; Specify the nice(2) priority to apply to the pool processes (only if set)
66 | ; The value can vary from -19 (highest priority) to 20 (lower priority)
67 | ; Note: - It will only work if the FPM master process is launched as root
68 | ; - The pool processes will inherit the master process priority
69 | ; unless it specified otherwise
70 | ; Default Value: no set
71 | ; process.priority = -19
72 |
73 | ; Set the process dumpable flag (PR_SET_DUMPABLE prctl) even if the process user
74 | ; or group is different than the master process user. It allows to create process
75 | ; core dump and ptrace the process for the pool user.
76 | ; Default Value: no
77 | ; process.dumpable = yes
78 |
79 | ; Choose how the process manager will control the number of child processes.
80 | ; Possible Values:
81 | ; static - a fixed number (pm.max_children) of child processes;
82 | ; dynamic - the number of child processes are set dynamically based on the
83 | ; following directives. With this process management, there will be
84 | ; always at least 1 children.
85 | ; pm.max_children - the maximum number of children that can
86 | ; be alive at the same time.
87 | ; pm.start_servers - the number of children created on startup.
88 | ; pm.min_spare_servers - the minimum number of children in 'idle'
89 | ; state (waiting to process). If the number
90 | ; of 'idle' processes is less than this
91 | ; number then some children will be created.
92 | ; pm.max_spare_servers - the maximum number of children in 'idle'
93 | ; state (waiting to process). If the number
94 | ; of 'idle' processes is greater than this
95 | ; number then some children will be killed.
96 | ; ondemand - no children are created at startup. Children will be forked when
97 | ; new requests will connect. The following parameter are used:
98 | ; pm.max_children - the maximum number of children that
99 | ; can be alive at the same time.
100 | ; pm.process_idle_timeout - The number of seconds after which
101 | ; an idle process will be killed.
102 | ; Note: This value is mandatory.
103 | pm = dynamic
104 |
105 | ; The number of child processes to be created when pm is set to 'static' and the
106 | ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
107 | ; This value sets the limit on the number of simultaneous requests that will be
108 | ; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
109 | ; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
110 | ; CGI. The below defaults are based on a server without much resources. Don't
111 | ; forget to tweak pm.* to fit your needs.
112 | ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
113 | ; Note: This value is mandatory.
114 | pm.max_children = 5
115 |
116 | ; The number of child processes created on startup.
117 | ; Note: Used only when pm is set to 'dynamic'
118 | ; Default Value: (min_spare_servers + max_spare_servers) / 2
119 | pm.start_servers = 2
120 |
121 | ; The desired minimum number of idle server processes.
122 | ; Note: Used only when pm is set to 'dynamic'
123 | ; Note: Mandatory when pm is set to 'dynamic'
124 | pm.min_spare_servers = 1
125 |
126 | ; The desired maximum number of idle server processes.
127 | ; Note: Used only when pm is set to 'dynamic'
128 | ; Note: Mandatory when pm is set to 'dynamic'
129 | pm.max_spare_servers = 3
130 |
131 | ; The number of seconds after which an idle process will be killed.
132 | ; Note: Used only when pm is set to 'ondemand'
133 | ; Default Value: 10s
134 | ;pm.process_idle_timeout = 10s;
135 |
136 | ; The number of requests each child process should execute before respawning.
137 | ; This can be useful to work around memory leaks in 3rd party libraries. For
138 | ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
139 | ; Default Value: 0
140 | ;pm.max_requests = 500
141 |
142 | ; The URI to view the FPM status page. If this value is not set, no URI will be
143 | ; recognized as a status page. It shows the following information:
144 | ; pool - the name of the pool;
145 | ; process manager - static, dynamic or ondemand;
146 | ; start time - the date and time FPM has started;
147 | ; start since - number of seconds since FPM has started;
148 | ; accepted conn - the number of request accepted by the pool;
149 | ; listen queue - the number of request in the queue of pending
150 | ; connections (see backlog in listen(2));
151 | ; max listen queue - the maximum number of requests in the queue
152 | ; of pending connections since FPM has started;
153 | ; listen queue len - the size of the socket queue of pending connections;
154 | ; idle processes - the number of idle processes;
155 | ; active processes - the number of active processes;
156 | ; total processes - the number of idle + active processes;
157 | ; max active processes - the maximum number of active processes since FPM
158 | ; has started;
159 | ; max children reached - number of times, the process limit has been reached,
160 | ; when pm tries to start more children (works only for
161 | ; pm 'dynamic' and 'ondemand');
162 | ; Value are updated in real time.
163 | ; Example output:
164 | ; pool: www
165 | ; process manager: static
166 | ; start time: 01/Jul/2011:17:53:49 +0200
167 | ; start since: 62636
168 | ; accepted conn: 190460
169 | ; listen queue: 0
170 | ; max listen queue: 1
171 | ; listen queue len: 42
172 | ; idle processes: 4
173 | ; active processes: 11
174 | ; total processes: 15
175 | ; max active processes: 12
176 | ; max children reached: 0
177 | ;
178 | ; By default the status page output is formatted as text/plain. Passing either
179 | ; 'html', 'xml' or 'json' in the query string will return the corresponding
180 | ; output syntax. Example:
181 | ; http://www.foo.bar/status
182 | ; http://www.foo.bar/status?json
183 | ; http://www.foo.bar/status?html
184 | ; http://www.foo.bar/status?xml
185 | ;
186 | ; By default the status page only outputs short status. Passing 'full' in the
187 | ; query string will also return status for each pool process.
188 | ; Example:
189 | ; http://www.foo.bar/status?full
190 | ; http://www.foo.bar/status?json&full
191 | ; http://www.foo.bar/status?html&full
192 | ; http://www.foo.bar/status?xml&full
193 | ; The Full status returns for each process:
194 | ; pid - the PID of the process;
195 | ; state - the state of the process (Idle, Running, ...);
196 | ; start time - the date and time the process has started;
197 | ; start since - the number of seconds since the process has started;
198 | ; requests - the number of requests the process has served;
199 | ; request duration - the duration in µs of the requests;
200 | ; request method - the request method (GET, POST, ...);
201 | ; request URI - the request URI with the query string;
202 | ; content length - the content length of the request (only with POST);
203 | ; user - the user (PHP_AUTH_USER) (or '-' if not set);
204 | ; script - the main script called (or '-' if not set);
205 | ; last request cpu - the %cpu the last request consumed
206 | ; it's always 0 if the process is not in Idle state
207 | ; because CPU calculation is done when the request
208 | ; processing has terminated;
209 | ; last request memory - the max amount of memory the last request consumed
210 | ; it's always 0 if the process is not in Idle state
211 | ; because memory calculation is done when the request
212 | ; processing has terminated;
213 | ; If the process is in Idle state, then informations are related to the
214 | ; last request the process has served. Otherwise informations are related to
215 | ; the current request being served.
216 | ; Example output:
217 | ; ************************
218 | ; pid: 31330
219 | ; state: Running
220 | ; start time: 01/Jul/2011:17:53:49 +0200
221 | ; start since: 63087
222 | ; requests: 12808
223 | ; request duration: 1250261
224 | ; request method: GET
225 | ; request URI: /test_mem.php?N=10000
226 | ; content length: 0
227 | ; user: -
228 | ; script: /home/fat/web/docs/php/test_mem.php
229 | ; last request cpu: 0.00
230 | ; last request memory: 0
231 | ;
232 | ; Note: There is a real-time FPM status monitoring sample web page available
233 | ; It's available in: @EXPANDED_DATADIR@/fpm/status.html
234 | ;
235 | ; Note: The value must start with a leading slash (/). The value can be
236 | ; anything, but it may not be a good idea to use the .php extension or it
237 | ; may conflict with a real PHP file.
238 | ; Default Value: not set
239 | ;pm.status_path = /status
240 |
241 | ; The address on which to accept FastCGI status request. This creates a new
242 | ; invisible pool that can handle requests independently. This is useful
243 | ; if the main pool is busy with long running requests because it is still possible
244 | ; to get the status before finishing the long running requests.
245 | ;
246 | ; Valid syntaxes are:
247 | ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
248 | ; a specific port;
249 | ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
250 | ; a specific port;
251 | ; 'port' - to listen on a TCP socket to all addresses
252 | ; (IPv6 and IPv4-mapped) on a specific port;
253 | ; '/path/to/unix/socket' - to listen on a unix socket.
254 | ; Default Value: value of the listen option
255 | ;pm.status_listen = 127.0.0.1:9001
256 |
257 | ; The ping URI to call the monitoring page of FPM. If this value is not set, no
258 | ; URI will be recognized as a ping page. This could be used to test from outside
259 | ; that FPM is alive and responding, or to
260 | ; - create a graph of FPM availability (rrd or such);
261 | ; - remove a server from a group if it is not responding (load balancing);
262 | ; - trigger alerts for the operating team (24/7).
263 | ; Note: The value must start with a leading slash (/). The value can be
264 | ; anything, but it may not be a good idea to use the .php extension or it
265 | ; may conflict with a real PHP file.
266 | ; Default Value: not set
267 | ;ping.path = /ping
268 |
269 | ; This directive may be used to customize the response of a ping request. The
270 | ; response is formatted as text/plain with a 200 response code.
271 | ; Default Value: pong
272 | ;ping.response = pong
273 |
274 | ; The access log file
275 | ; Default: not set
276 | ;access.log = log/$pool.access.log
277 |
278 | ; The access log format.
279 | ; The following syntax is allowed
280 | ; %%: the '%' character
281 | ; %C: %CPU used by the request
282 | ; it can accept the following format:
283 | ; - %{user}C for user CPU only
284 | ; - %{system}C for system CPU only
285 | ; - %{total}C for user + system CPU (default)
286 | ; %d: time taken to serve the request
287 | ; it can accept the following format:
288 | ; - %{seconds}d (default)
289 | ; - %{milliseconds}d
290 | ; - %{mili}d
291 | ; - %{microseconds}d
292 | ; - %{micro}d
293 | ; %e: an environment variable (same as $_ENV or $_SERVER)
294 | ; it must be associated with embraces to specify the name of the env
295 | ; variable. Some examples:
296 | ; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
297 | ; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
298 | ; %f: script filename
299 | ; %l: content-length of the request (for POST request only)
300 | ; %m: request method
301 | ; %M: peak of memory allocated by PHP
302 | ; it can accept the following format:
303 | ; - %{bytes}M (default)
304 | ; - %{kilobytes}M
305 | ; - %{kilo}M
306 | ; - %{megabytes}M
307 | ; - %{mega}M
308 | ; %n: pool name
309 | ; %o: output header
310 | ; it must be associated with embraces to specify the name of the header:
311 | ; - %{Content-Type}o
312 | ; - %{X-Powered-By}o
313 | ; - %{Transfert-Encoding}o
314 | ; - ....
315 | ; %p: PID of the child that serviced the request
316 | ; %P: PID of the parent of the child that serviced the request
317 | ; %q: the query string
318 | ; %Q: the '?' character if query string exists
319 | ; %r: the request URI (without the query string, see %q and %Q)
320 | ; %R: remote IP address
321 | ; %s: status (response code)
322 | ; %t: server time the request was received
323 | ; it can accept a strftime(3) format:
324 | ; %d/%b/%Y:%H:%M:%S %z (default)
325 | ; The strftime(3) format must be encapsuled in a %{}t tag
326 | ; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
327 | ; %T: time the log has been written (the request has finished)
328 | ; it can accept a strftime(3) format:
329 | ; %d/%b/%Y:%H:%M:%S %z (default)
330 | ; The strftime(3) format must be encapsuled in a %{}t tag
331 | ; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
332 | ; %u: remote user
333 | ;
334 | ; Default: "%R - %u %t \"%m %r\" %s"
335 | ;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
336 |
337 | ; The log file for slow requests
338 | ; Default Value: not set
339 | ; Note: slowlog is mandatory if request_slowlog_timeout is set
340 | ;slowlog = log/$pool.log.slow
341 |
342 | ; The timeout for serving a single request after which a PHP backtrace will be
343 | ; dumped to the 'slowlog' file. A value of '0s' means 'off'.
344 | ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
345 | ; Default Value: 0
346 | ;request_slowlog_timeout = 0
347 |
348 | ; Depth of slow log stack trace.
349 | ; Default Value: 20
350 | ;request_slowlog_trace_depth = 20
351 |
352 | ; The timeout for serving a single request after which the worker process will
353 | ; be killed. This option should be used when the 'max_execution_time' ini option
354 | ; does not stop script execution for some reason. A value of '0' means 'off'.
355 | ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
356 | ; Default Value: 0
357 | ;request_terminate_timeout = 0
358 |
359 | ; The timeout set by 'request_terminate_timeout' ini option is not engaged after
360 | ; application calls 'fastcgi_finish_request' or when application has finished and
361 | ; shutdown functions are being called (registered via register_shutdown_function).
362 | ; This option will enable timeout limit to be applied unconditionally
363 | ; even in such cases.
364 | ; Default Value: no
365 | ;request_terminate_timeout_track_finished = no
366 |
367 | ; Set open file descriptor rlimit.
368 | ; Default Value: system defined value
369 | ;rlimit_files = 1024
370 |
371 | ; Set max core size rlimit.
372 | ; Possible Values: 'unlimited' or an integer greater or equal to 0
373 | ; Default Value: system defined value
374 | ;rlimit_core = 0
375 |
376 | ; Chroot to this directory at the start. This value must be defined as an
377 | ; absolute path. When this value is not set, chroot is not used.
378 | ; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
379 | ; of its subdirectories. If the pool prefix is not set, the global prefix
380 | ; will be used instead.
381 | ; Note: chrooting is a great security feature and should be used whenever
382 | ; possible. However, all PHP paths will be relative to the chroot
383 | ; (error_log, sessions.save_path, ...).
384 | ; Default Value: not set
385 | ;chroot =
386 |
387 | ; Chdir to this directory at the start.
388 | ; Note: relative path can be used.
389 | ; Default Value: current directory or / when chroot
390 | ;chdir = /var/www
391 |
392 | ; Redirect worker stdout and stderr into main error log. If not set, stdout and
393 | ; stderr will be redirected to /dev/null according to FastCGI specs.
394 | ; Note: on highloaded environment, this can cause some delay in the page
395 | ; process time (several ms).
396 | ; Default Value: no
397 | ;catch_workers_output = yes
398 |
399 | ; Decorate worker output with prefix and suffix containing information about
400 | ; the child that writes to the log and if stdout or stderr is used as well as
401 | ; log level and time. This options is used only if catch_workers_output is yes.
402 | ; Settings to "no" will output data as written to the stdout or stderr.
403 | ; Default value: yes
404 | ;decorate_workers_output = no
405 |
406 | ; Clear environment in FPM workers
407 | ; Prevents arbitrary environment variables from reaching FPM worker processes
408 | ; by clearing the environment in workers before env vars specified in this
409 | ; pool configuration are added.
410 | ; Setting to "no" will make all environment variables available to PHP code
411 | ; via getenv(), $_ENV and $_SERVER.
412 | ; Default Value: yes
413 | ;clear_env = no
414 |
415 | ; Limits the extensions of the main script FPM will allow to parse. This can
416 | ; prevent configuration mistakes on the web server side. You should only limit
417 | ; FPM to .php extensions to prevent malicious users to use other extensions to
418 | ; execute php code.
419 | ; Note: set an empty value to allow all extensions.
420 | ; Default Value: .php
421 | ;security.limit_extensions = .php .php3 .php4 .php5 .php7
422 |
423 | ; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
424 | ; the current environment.
425 | ; Default Value: clean env
426 | ;env[HOSTNAME] = $HOSTNAME
427 | ;env[PATH] = /usr/local/bin:/usr/bin:/bin
428 | ;env[TMP] = /tmp
429 | ;env[TMPDIR] = /tmp
430 | ;env[TEMP] = /tmp
431 |
432 | ; Additional php.ini defines, specific to this pool of workers. These settings
433 | ; overwrite the values previously defined in the php.ini. The directives are the
434 | ; same as the PHP SAPI:
435 | ; php_value/php_flag - you can set classic ini defines which can
436 | ; be overwritten from PHP call 'ini_set'.
437 | ; php_admin_value/php_admin_flag - these directives won't be overwritten by
438 | ; PHP call 'ini_set'
439 | ; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.
440 |
441 | ; Defining 'extension' will load the corresponding shared extension from
442 | ; extension_dir. Defining 'disable_functions' or 'disable_classes' will not
443 | ; overwrite previously defined php.ini values, but will append the new value
444 | ; instead.
445 |
446 | ; Note: path INI options can be relative and will be expanded with the prefix
447 | ; (pool, global or @prefix@)
448 |
449 | ; Default Value: nothing is defined by default except the values in php.ini and
450 | ; specified at startup with the -d argument
451 | ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
452 | ;php_flag[display_errors] = off
453 | ;php_admin_value[error_log] = /var/log/fpm-php.www.log
454 | ;php_admin_flag[log_errors] = on
455 | ;php_admin_value[memory_limit] = 32M
--------------------------------------------------------------------------------