├── .gitignore ├── README.md ├── app.php ├── cache └── .gitkeep ├── composer.json ├── conf ├── routing.yml ├── services.yml ├── services_dev.yml └── services_prod.yml ├── log └── .gitkeep └── src └── App ├── AppKernel.php ├── Command └── RunCommand.php ├── Controller └── IndexController.php └── PromiseResponse.php /.gitignore: -------------------------------------------------------------------------------- 1 | /cache 2 | /composer.lock 3 | /log 4 | /vendor 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactPHP + Symfony example 2 | 3 | **[`services.yml`](conf/services.yml)** 4 | 5 | Base service definitions to enable Symfony to route requests, resolve controllers etc. 6 | 7 | Most important lines: 8 | 9 | ```yaml 10 | event_dispatcher: 11 | # ... 12 | calls: 13 | # ... 14 | - [ addListener, [ kernel.view, [ App\PromiseResponse, wrapPromise ] ] ] 15 | ``` 16 | 17 | `PromiseResponse` allows Symfony to return *something*. That something is a promise of the actual response. 18 | 19 | **[`RunCommand.php`](src/App/Command/RunCommand.php)** 20 | 21 | Starts HTTP server that converts ReactPHP requests to Symfony requests and then Symfony responses to ReactPHP responses. 22 | 23 | **[`IndexController.php`](src/App/Controller/IndexController.php)** 24 | 25 | An example controller. `indexAction` returns immediately response. `promiseAction` responds after X seconds waiting. 26 | 27 | --- 28 | 29 | Run with: 30 | 31 | ```sh 32 | $ ./app.php run 33 | ``` -------------------------------------------------------------------------------- /app.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new RunCommand()); 15 | $app->run(); 16 | -------------------------------------------------------------------------------- /cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubkulhan/reactphp-symfony/ecbff5c531be2784975ff21cc0bf4304ce26ad12/cache/.gitkeep -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "php": ">=5.4", 4 | "react/react": "~0.4", 5 | "skrz/autowiring-bundle": "~1.1", 6 | "symfony/config": "~2.7", 7 | "symfony/console": "~2.7", 8 | "symfony/dependency-injection": "~2.7", 9 | "symfony/framework-bundle": "~2.7", 10 | "symfony/http-foundation": "~2.7", 11 | "symfony/http-kernel": "~2.7", 12 | "symfony/routing": "~2.7", 13 | "symfony/yaml": "~2.7" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "App\\": [ 18 | "src/App/" 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /conf/routing.yml: -------------------------------------------------------------------------------- 1 | index: 2 | path: / 3 | defaults: 4 | _controller: controller.index:indexAction 5 | 6 | promise: 7 | path: /promise/{secs} 8 | defaults: 9 | _controller: controller.index:promiseAction 10 | -------------------------------------------------------------------------------- /conf/services.yml: -------------------------------------------------------------------------------- 1 | parameters: ~ 2 | 3 | autowiring: 4 | autoscan_psr4: 5 | App: %kernel.root_dir%/src/App 6 | 7 | services: 8 | # react 9 | react.loop: 10 | class: React\EventLoop\LoopInterface 11 | synthetic: true 12 | 13 | # base application services 14 | kernel: 15 | class: Symfony\Component\HttpKernel\Kernel 16 | synthetic: true 17 | 18 | service_container: 19 | class: Symfony\Component\DependencyInjection\Container 20 | synthetic: true 21 | 22 | http_kernel: 23 | class: Symfony\Component\HttpKernel\HttpKernel 24 | 25 | event_dispatcher: 26 | class: Symfony\Component\EventDispatcher\EventDispatcher 27 | calls: 28 | - [ addSubscriber, [ @router_listener ] ] 29 | - [ addListener, [ kernel.view, [ App\PromiseResponse, wrapPromise ] ] ] 30 | 31 | controller_resolver: 32 | class: Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver 33 | 34 | controller_name_parser: 35 | class: Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser 36 | 37 | router: 38 | class: Symfony\Bundle\FrameworkBundle\Routing\Router 39 | arguments: [ @service_container, %kernel.root_dir%/conf/routing.yml ] 40 | 41 | router_listener: 42 | class: Symfony\Component\HttpKernel\EventListener\RouterListener 43 | arguments: [ @router, ~, ~, ~ ] 44 | 45 | file_locator: 46 | class: Symfony\Component\HttpKernel\Config\FileLocator 47 | arguments: [ @kernel, %kernel.root_dir%/conf/hitserver ] 48 | 49 | routing.loader: 50 | class: Symfony\Component\Routing\Loader\YamlFileLoader 51 | -------------------------------------------------------------------------------- /conf/services_dev.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: services.yml } 3 | 4 | parameters: ~ 5 | -------------------------------------------------------------------------------- /conf/services_prod.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: services.yml } 3 | 4 | parameters: ~ 5 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakubkulhan/reactphp-symfony/ecbff5c531be2784975ff21cc0bf4304ce26ad12/log/.gitkeep -------------------------------------------------------------------------------- /src/App/AppKernel.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class AppKernel extends Kernel 12 | { 13 | 14 | public function getRootDir() 15 | { 16 | return __DIR__ . "/../.."; 17 | } 18 | 19 | public function getLogDir() 20 | { 21 | return $this->getRootDir() . "/log"; 22 | } 23 | 24 | public function registerBundles() 25 | { 26 | return [ 27 | new SkrzAutowiringBundle(), 28 | ]; 29 | } 30 | 31 | public function registerContainerConfiguration(LoaderInterface $loader) 32 | { 33 | $loader->load($this->getRootDir() . "/conf/services_" . $this->getEnvironment() . ".yml"); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/App/Command/RunCommand.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class RunCommand extends Command 24 | { 25 | 26 | protected function configure() 27 | { 28 | $this 29 | ->setName("run") 30 | ->setDescription("Run app server") 31 | ->addOption("host", null, InputOption::VALUE_REQUIRED, "Host to bind HTTP server to.", "127.0.0.1") 32 | ->addOption("port", null, InputOption::VALUE_REQUIRED, "Port to bind HTTP server to.", 8080) 33 | ->addOption("environment", "e", InputOption::VALUE_REQUIRED, "App server kernel environment.", "dev"); 34 | } 35 | 36 | 37 | protected function execute(InputInterface $input, OutputInterface $output) 38 | { 39 | $kernel = new AppKernel($environment = $input->getOption("environment"), $environment !== "prod"); 40 | $kernel->boot(); 41 | 42 | $loop = Factory::create(); 43 | 44 | /** @var Container $container */ 45 | $container = $kernel->getContainer(); 46 | $container->set("react.loop", $loop); 47 | 48 | $socket = new Socket($loop); 49 | $http = new Server($socket); 50 | 51 | $http->on("request", function (Request $request, Response $response) use ($kernel, $loop) { 52 | $headers = $request->getHeaders(); 53 | $cookies = []; 54 | 55 | if (isset($headers["Cookie"])) { 56 | foreach ((array)$headers["Cookie"] as $cookieHeader) { 57 | foreach (explode(";", $cookieHeader) as $cookie) { 58 | list($name, $value) = explode("=", trim($cookie), 2); 59 | $cookies[$name] = urldecode($value); 60 | } 61 | } 62 | } 63 | 64 | $symfonyRequest = new SymfonyRequest( 65 | $request->getQuery(), 66 | [], // TODO: handle post data 67 | [], 68 | $cookies, 69 | [], 70 | [ 71 | "REQUEST_URI" => $request->getPath(), 72 | "SERVER_NAME" => explode(":", $headers["Host"])[0], 73 | "REMOTE_ADDR" => $request->remoteAddress, 74 | "QUERY_STRING" => http_build_query($request->getQuery()), 75 | ], 76 | null // TODO: handle post data 77 | ); 78 | 79 | $symfonyRequest->headers->replace($headers); 80 | 81 | $symfonyResponse = $kernel->handle($symfonyRequest); 82 | 83 | if ($kernel instanceof TerminableInterface) { 84 | $kernel->terminate($symfonyRequest, $symfonyResponse); 85 | } 86 | 87 | if ($symfonyResponse instanceof PromiseInterface) { 88 | $symfonyResponse->then(function (SymfonyResponse $symfonyResponse) use ($response) { 89 | $this->send($response, $symfonyResponse); 90 | 91 | }, function ($error) use ($loop, $response) { 92 | echo "Exception: ", (string) $error, "\n"; 93 | 94 | $response->writeHead(500, ["Content-Type" => "text/plain"]); 95 | $response->end("500 Internal Server Error"); 96 | $loop->stop(); 97 | }); 98 | 99 | } elseif ($symfonyResponse instanceof SymfonyResponse) { 100 | $this->send($response, $symfonyResponse); 101 | 102 | } else { 103 | echo "Unsupported response type: ", get_class($symfonyResponse), "\n"; 104 | 105 | $response->writeHead(500, ["Content-Type" => "text/plain"]); 106 | $response->end("500 Internal Server Error"); 107 | $loop->stop(); 108 | } 109 | }); 110 | 111 | $socket->listen($port = $input->getOption("port"), $host = $input->getOption("host")); 112 | 113 | echo "Listening to {$host}:{$port}\n"; 114 | 115 | $loop->run(); 116 | } 117 | 118 | private function send(Response $res, SymfonyResponse $symfonyResponse) 119 | { 120 | $headers = $symfonyResponse->headers->allPreserveCase(); 121 | $headers["X-Powered-By"] = "Love"; 122 | 123 | $cookies = $symfonyResponse->headers->getCookies(); 124 | if (count($cookies)) { 125 | $headers["Set-Cookie"] = []; 126 | foreach ($symfonyResponse->headers->getCookies() as $cookie) { 127 | $headers["Set-Cookie"][] = (string)$cookie; 128 | } 129 | } 130 | 131 | $res->writeHead($symfonyResponse->getStatusCode(), $headers); 132 | $res->end($symfonyResponse->getContent()); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/App/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @Controller 15 | */ 16 | class IndexController 17 | { 18 | 19 | /** 20 | * @var LoopInterface 21 | * 22 | * @Autowired 23 | */ 24 | public $loop; 25 | 26 | public function indexAction(Request $request) 27 | { 28 | return Response::create("Hello, world!\n"); 29 | } 30 | 31 | public function promiseAction(Request $request) 32 | { 33 | $secs = intval($request->attributes->get("secs")); 34 | 35 | $deferred = new Deferred(); 36 | 37 | $this->loop->addTimer($secs, function () use ($secs, $deferred) { 38 | $deferred->resolve(Response::create("{$secs} seconds later...\n")); 39 | }); 40 | 41 | return $deferred->promise(); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/App/PromiseResponse.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class PromiseResponse extends Response implements PromiseInterface 12 | { 13 | 14 | /** @var PromiseInterface */ 15 | private $wrappedPromise; 16 | 17 | public function __construct(PromiseInterface $wrappedPromise) 18 | { 19 | $this->wrappedPromise = $wrappedPromise; 20 | } 21 | 22 | public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) 23 | { 24 | return $this->wrappedPromise->then($onFulfilled, $onRejected, $onProgress); 25 | } 26 | 27 | public static function wrapPromise(GetResponseForControllerResultEvent $event) 28 | { 29 | if (!$event->hasResponse() && $event->getControllerResult() instanceof PromiseInterface) { 30 | $event->setResponse(new self($event->getControllerResult())); 31 | } 32 | } 33 | 34 | } 35 | --------------------------------------------------------------------------------