Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for the Symfony kernel #85

Merged
merged 1 commit into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ chapter)
- [Workers](#workers)
- [Watcher](#watcher)
- [Static server](#static-server)
- [Symfony bridge](#symfony-bridge)
- [DriftPHP resources](#driftphp-resources)

## Installation

Expand Down Expand Up @@ -190,6 +192,21 @@ In this example, a file named `app.js` located under `/internal/public/path/`
folder will be accessible at `http://localhost:8000/public/app.js`. By default,
this feature is disabled.

## Symfony bridge

In order to help you from migrating an application from Symfony to DriftPHP,
assuming that this means that your whole domain should turn on top of Promises,
including your infrastructure layer, this server is distributed with a small
Symfony adapter. Use it as a tool, and never use it at production (using a
ReactPHP based server in a blocking application is something not recommendable
at all in terms of performance and service availability). That adapter will help
your migrating from one platform to the other, as will allow this server to work
with your Symfony kernel.

```bash
php vendor/bin/server watch 0.0.0.0:8000 --adapter=symfony
```

## DriftPHP resources

Some first steps for you!
Expand Down
310 changes: 17 additions & 293 deletions src/Adapter/DriftKernel/DriftKernelAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,98 +15,29 @@

namespace Drift\Server\Adapter\DriftKernel;

use Drift\Console\OutputPrinter;
use Drift\EventBus\Subscriber\EventBusSubscriber;
use Drift\HttpKernel\AsyncKernel;
use Drift\Kernel as ApplicationKernel;
use Drift\Server\Adapter\KernelAdapter;
use Drift\Server\Context\ServerContext;
use Drift\Server\Exception\KernelException;
use Drift\Server\Exception\RouteNotFoundException;
use Drift\Server\Mime\MimeTypeChecker;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface as PsrUploadedFile;
use React\EventLoop\LoopInterface;
use React\Filesystem\FilesystemInterface;
use React\Http\Message\Response as ReactResponse;
use function React\Promise\all;
use Drift\Server\Adapter\SymfonyKernelBasedAdapter;
use Exception;
use React\Promise\PromiseInterface;
use function React\Promise\resolve;
use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Exception\RouteNotFoundException as SymfonyRouteNotFoundException;
use Throwable;
use Symfony\Component\HttpKernel\Kernel;

/**
* Class DriftKernelAdapter.
*/
class DriftKernelAdapter implements KernelAdapter
class DriftKernelAdapter extends SymfonyKernelBasedAdapter
{
private AsyncKernel $kernel;
private FilesystemInterface $filesystem;
private ServerContext $serverContext;
private MimeTypeChecker $mimeTypeChecker;
private string $rootPath;

/**
* @param LoopInterface $loop
* @param string $rootPath
* @param ServerContext $serverContext
* @param FilesystemInterface $filesystem
* @param OutputPrinter $outputPrinter
* @param MimeTypeChecker $mimeTypeChecker
*
* @return PromiseInterface<self>
* @param $kernel
*
* @throws KernelException
* @throws Exception
*/
public static function create(
LoopInterface $loop,
string $rootPath,
ServerContext $serverContext,
FilesystemInterface $filesystem,
OutputPrinter $outputPrinter,
MimeTypeChecker $mimeTypeChecker
): PromiseInterface {
$adapter = new self();
$kernel = static::createKernelByEnvironmentAndDebug($serverContext->getEnvironment(), $serverContext->isDebug());
protected function checkKernel($kernel)
{
if (!$kernel instanceof AsyncKernel) {
throw SyncKernelException::build();
throw new SyncKernelException('The kernel should implement AsyncKernel interface, as you are using the DriftPHP kernel adapter');
}

$kernel->boot();
$kernel
->getContainer()
->set('reactphp.event_loop', $loop);

$adapter->kernel = $kernel;
$adapter->serverContext = $serverContext;
$adapter->filesystem = $filesystem;
$adapter->mimeTypeChecker = $mimeTypeChecker;
$adapter->rootPath = $rootPath;

return $kernel
->preload()
->then(function () use ($adapter, $outputPrinter) {
$container = $adapter->kernel->getContainer();
$serverContext = $adapter->serverContext;

if (
$serverContext->hasExchanges() &&
$container->has(EventBusSubscriber::class)
) {
$eventBusSubscriber = $container->get(EventBusSubscriber::class);
$eventBusSubscriber->subscribeToExchanges(
$serverContext->getExchanges(),
$outputPrinter
);
}
})
->then(function () use ($adapter) {
return $adapter;
});
}

/**
Expand All @@ -123,60 +54,16 @@ protected static function createKernelByEnvironmentAndDebug(
}

/**
* @param ServerRequestInterface $request
*
* @return PromiseInterface<ResponseInterface>
*/
public function handle(ServerRequestInterface $request): PromiseInterface
{
$uriPath = $request->getUri()->getPath();
$method = $request->getMethod();

return
$this->toSymfonyRequest(
$request,
$method,
$uriPath
)
->then(function (Request $symfonyRequest) use ($request) {
return all([
resolve($symfonyRequest),
$this->kernel->handleAsync($symfonyRequest),
])
->otherwise(function (SymfonyRouteNotFoundException $symfonyRouteNotFoundException) {
throw new RouteNotFoundException($symfonyRouteNotFoundException->getMessage());
})
->then(function (array $parts) use ($request) {
list($symfonyRequest, $symfonyResponse) = $parts;

/*
* We don't have to wait to this clean
*/
$this->cleanTemporaryUploadedFiles($symfonyRequest);
$symfonyRequest = null;

$response = $symfonyResponse;
if ($response instanceof Response) {
$response = new ReactResponse(
$response->getStatusCode(),
$response->headers->all(),
$response->getContent()
);
}

return $response;
});
});
}

/**
* Get static folder by kernel.
* @param Kernel|AsyncKernel $kernel
* @param Request $request
*
* @return string|null
* @return PromiseInterface
*/
public static function getStaticFolder(): ? string
{
return '/public';
protected function kernelHandle(
Kernel $kernel,
Request $request
): PromiseInterface {
return $kernel->handleAsync($request);
}

/**
Expand All @@ -196,167 +83,4 @@ public static function getObservableFolders(): array
{
return ['Drift', 'src', 'public', 'views'];
}

/**
* Get watcher folders.
*
* @return string[]
*/
public static function getObservableExtensions(): array
{
return ['php', 'yml', 'yaml', 'xml', 'css', 'js', 'html', 'twig'];
}

/**
* Get watcher ignoring folders.
*
* @return string[]
*/
public static function getIgnorableFolders(): array
{
return [];
}

/**
* Http request to symfony request.
*
* @param ServerRequestInterface $request
* @param string $method
* @param string $uriPath
*
* @return PromiseInterface<Request>
*/
private function toSymfonyRequest(
ServerRequestInterface $request,
string $method,
string $uriPath
): PromiseInterface {
$allowFileUploads = !$this
->serverContext
->areFileUploadsDisabled();

$uploadedFiles = $allowFileUploads
? array_map(function (PsrUploadedFile $file) {
return $this->toSymfonyUploadedFile($file);
}, $request->getUploadedFiles())
: [];

return all($uploadedFiles)
->then(function (array $uploadedFiles) use ($request, $method, $uriPath) {
$uploadedFiles = array_filter($uploadedFiles);
$headers = $request->getHeaders();
$isNotTransferEncoding = !array_key_exists('Transfer-Encoding', $headers);

$bodyParsed = [];
$bodyContent = '';
if ($isNotTransferEncoding) {
$bodyParsed = $request->getParsedBody() ?? [];
$bodyContent = $request->getBody()->getContents();
}

$symfonyRequest = new Request(
$request->getQueryParams(),
$bodyParsed,
$request->getAttributes(),
$this->serverContext->areCookiesDisabled()
? []
: $request->getCookieParams(),
$uploadedFiles,
$_SERVER,
$bodyContent
);

$symfonyRequest->setMethod($method);
$symfonyRequest->headers->replace($headers);

$symfonyRequest->server->replace(
$request->getServerParams()
+ ['REQUEST_URI' => $uriPath]
+ $symfonyRequest->server->all()
);

if ($symfonyRequest->headers->has('authorization') &&
0 === stripos($symfonyRequest->headers->get('authorization'), 'basic ')) {
$exploded = explode(':', base64_decode(substr($symfonyRequest->headers->get('authorization'), 6)), 2);
if (2 == \count($exploded)) {
list($basicAuthUsername, $basicAuthPassword) = $exploded;
$symfonyRequest->headers->set('PHP_AUTH_USER', $basicAuthUsername);
$symfonyRequest->headers->set('PHP_AUTH_PW', $basicAuthPassword);
}
}

$symfonyRequest->attributes->set('body', $request->getBody());

if (isset($headers['Host'])) {
$symfonyRequest->server->set('SERVER_NAME', explode(':', $headers['Host'][0]));
}

return $symfonyRequest;
});
}

/**
* PSR Uploaded file to Symfony file.
*
* @param PsrUploadedFile $file
*
* @return PromiseInterface<SymfonyUploadedFile>
*/
private function toSymfonyUploadedFile(PsrUploadedFile $file): PromiseInterface
{
if (UPLOAD_ERR_NO_FILE == $file->getError()) {
return resolve(new SymfonyUploadedFile(
'',
$file->getClientFilename(),
$file->getClientMediaType(),
$file->getError(),
true
));
}

$filename = $file->getClientFilename();
$extension = $this->mimeTypeChecker->getExtension($filename);
$tmpFilename = sys_get_temp_dir().'/'.md5(uniqid((string) rand(), true)).'.'.$extension;

try {
$content = $file
->getStream()
->getContents();
} catch (Throwable $throwable) {
return resolve(false);
}

$promise = (UPLOAD_ERR_OK == $file->getError())
? $this
->filesystem
->file($tmpFilename)
->putContents($content)
: resolve();

return $promise
->then(function () use ($file, $tmpFilename, $filename) {
return new SymfonyUploadedFile(
$tmpFilename,
$filename,
$file->getClientMediaType(),
$file->getError(),
true
);
});
}

/**
* @param Request $request
*
* @return PromiseInterface[]
*/
private function cleanTemporaryUploadedFiles(Request $request): array
{
return array_map(function (SymfonyUploadedFile $file) {
return $this
->filesystem
->file($file->getPath().'/'.$file->getFilename())
->remove();
}, $request->files->all());
}
}
Loading