laravel / ideas

Issues board used for Laravel internals discussions.
940 stars 28 forks source link

React-PHP Laravel Integration #464

Closed halaei closed 6 years ago

halaei commented 7 years ago

It will be nice if Laravel has Request::captureReactRequest() and Response::sendThroughReact() methods so that the following code could actually work:

$kernel = app()->make(Illuminate\Contracts\Http\Kernel::class);
$react = function ($request, $response) use ($kernel) {
    $kernel->handle(Illuminate\Http\Request::captureReactRequest($request))->sendThroughReact($response);
};
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);
$http->on('request', $react);
$socket->listen(1337);
$loop->run();
matthewtrask commented 7 years ago

I would say no. Not everyone uses/wants/needs React/Async PHP in their applications, so this is unneeded overhead in the application.

sebastiandedeyne commented 7 years ago

Could't this be done by extending Request or adding methods via Macroable?

halaei commented 7 years ago

Not everyone uses React. But performance matters and everyone needs that iff there is a clean implementation.

tomschlick commented 7 years ago

At best, I would see this as an official package like passport, echo, etc. However you'd need to make a strong use case for it for Taylor to undertake the work of maintaining something like that.

franzliedke commented 7 years ago

Please have a look at #102 - PHP-PM is using ReactPHP und the hood. That other issue also links to the existing php-pm-httpkernel bridge, which includes an adapter for Laravel.

However, in the framework's current state, the bridge has to jump through a few hoops to use Laravel properly - and I believe not all problems have been fixed so far.

With more async libraries popping up, and with PHP 7 released (with exceptions replacing many fatal errors), this more "classic" model of running a HTTP server within the app process is becoming more realistic in PHP. It would be super cool if we could move towards fully separating the framework (the part that boots up all the included libraries etc., only needs to be run once) from the request lifecycle (which is run for every request) within the next Laravel release(s). As PHP-PM shows, there is lots of potential for real performance gains there.

Mevrael commented 7 years ago

I've created a gist on how to create a WebSocket server integrated with Laravel so you could use Eloquent, Request and other goodies, however, session sometimes still is not recognized and user might not be authenticated in WebSocketController but overall session is also passed from WebSocket server to Laravel

https://gist.github.com/Mevrael/6855dd47d45fa34ee7161c8e0d2d0e88

halaei commented 7 years ago

I wish for an easy-to-setup solution shipped with Laravel 5.5 to eliminate PHP&Laravel framework bootstrap time for each request. Having that achieved, maybe PHP frameworks should gradually move towards handling I/O the way JS applications does. As far as I can guess, the whole magic is implementable via the select system call, that is available in PHP by stream_select function.

Mevrael commented 7 years ago

I also would say that Web Sockets in 2017 is a must-have and Laravel should offer an inhousee solution out of the box, however, PHP still can't be used exactly same way as Node.JS per architecture, nevertheless, things going forward and on the first place I would suggest you to participate in ReactPHP/Ratchet discussion and development and to figure out ways on making it possible for PHP in the future but I believe there would be needed changes in PHP Core as well.

In any case Laravel as all other old existing PHP frameworks won't go async as Node.js frameworks since it would be a lot of changes in the architecture. I believe that new framework might arise here and become a mainstream.

If Laravel bootstrapping is an issue for you take a look on Lumen or just make inhouse solution on the top of ReactPHP/Ratchet.

happyDemon commented 7 years ago

For anyone wondering, there's actually a node.js framework out there that mimics laravel in a lot of ways: http://www.adonisjs.com/

franzliedke commented 7 years ago

@halaei @Mevrael I actually don't think that this will be all that hard to achieve. The most important thing is to keep bootstrap-related and request-related things apart from each other. This mostly affects things like request, session and auth.

The second step is extending the documentation and educating the community about this separation.

In the end, Laravel would be easily usable with a solution like PHP-PM, just like you can drop it into a classical webserver that supports e.g. FPM.

barryvdh commented 7 years ago

I think https://github.com/php-pm/php-pm (with https://github.com/php-pm/php-pm-httpkernel ) is probably the way to go. I suggest that you take a look at what is needed to make it work, then perhaps we have some concrete things to work on.

(Also, some related topics like websockets and nodeJS aren't really the issue here, right?)

ajcastro commented 7 years ago

@happyDemon , how can i use adonisjs in the browser? is that possible? so that I can reuse the models in the browser.

franzliedke commented 7 years ago

@barryvdh I am more than happy to start looking into this, but I would at least want to hear from @taylorotwell whether he is interested in going in this direction (enabling Laravel to work seamlessly with different process models) first.

happyDemon commented 7 years ago

@ajcastro It's a node.js framework, which, just like laravel, is targeted at back-end developers.

@barryvdh sorry for dropping it in, however when I think of react-php, I always think of using it for websockets. In any case I thought of suggesting adonis since it does cover the general use case, albeit it's written in js.

pstephan1187 commented 7 years ago

I would like to see Laravel implement websockets natively. Having to use Pusher or redis or some other thing just adds another piece of software that has to be maintained and watched. I think a Laravel package (like Echo, or Spark) would be nice so that it wouldn't add overhead to projects that don't need it, but could be added in as necessary for those that do.

taylorotwell commented 6 years ago

This feels like such a general issue I'm not sure what action I can take from here.

sisve commented 6 years ago

There are 10 other participants in this thread. Even if only half of them knows how to proceed, how is closing the issue an proper action at this time? Is it a statement that this feature will not be accepted?

zuohuadong commented 6 years ago

https://github.com/nestjs/nest is better than adonisjs . Be aware that most phper levels are still at an early stage. Even with Async, the corresponding ecology is difficult to build. PHP use async and requires a lot of expansion to be installed, which is too difficult for most phper.

The use of Node.js or Golang will be simpler, not to mention, more and more large enterprises left the PHP, so, why do these developers have to choose PHP? @happyDemon

happyDemon commented 6 years ago

@zuohuadong I Don't know nest, I suggested adonisjs because of how familiar it feels next to laravel, making it easier to switch over to if needed.

Running php in async is not something that hard to set up, the tools around it are also there, if you take a look at swoole for example (which also has an integration for laravel), it's also not that hard to use if you follow the docs. Sure you'll have to tweak the way you think, but that's just as true if a php developer would switch to nodejs or golang.

However it might be harder, since the community around these tools/methodologies are smaller within the PHP community.

In the end you're never forced to choose something, you can choose whichever feels good to you, I find that that makes it easier to learn something new and more enjoyable to keep on using it.

zuohuadong commented 6 years ago

@happyDemon We've tried reactphp and Swoole, and we've made an open source demo. Unfortunately, most phper do not install the environment properly. Compared to the Nodejs npm install, Golang's go got, phpize pecl composer use is really troublesome, and need to modify php.ini, a lot of people are stuck here.

And, I don't think there will be big companies in the future to continue to invest in PHP, even Facebook, there are many node.js open source projects.

In addition, PHP7 introduced a lot of new things, such as the strong type have few users, But Typescript has many fans. I don't think it's a good thing for phper to introduce new concepts, because most phper don't need them at all.

I think it's still the status quo before PHP makes any major changes. JIT has been bothering it long enough, I don't think it can do better than V8, in addition, we no longer need node-in-php.

Thanks.

aftabnaveed commented 6 years ago

I am not sure if it makes sense to float the built in HTTP Server idea, apologies in advance if it does not make sense. In my opinion Laravel should come up with its own HTTP server using ReactPHP. Booting the framework only once will not only boost the performance at least 10x faster. It also will pave the way for async programming without relying on 3rd party applications. I know there is PHP-PM, but all other major frameworks in other languages such as Django, ROR, Java Spring Boot etc has its own builtin servers making it easier to get started.

zuohuadong commented 6 years ago

@aftabnaveed This is a good idea. But for many phpers, it's too difficult. Do we need laravel for node.js?

aftabnaveed commented 6 years ago

Do we need laravel for node.js?

@zuohuadong Sorry, I don't get your point

zuohuadong commented 6 years ago

@aftabnaveed laravel (php-fpm) , laravel (node.js)

aftabnaveed commented 6 years ago

I think node.js is good for the frontend, and in my projects I am no longer using "blade" I already have replaced it with ReactJS SSR (node.js). Using PHP for business logic and node for the view you get best of the both.

However, that's not the point here. Having a built in HTTP server will rather make it easier for phpers to get started by removing the necessity of configuring nginx or apache. All they would need is php artisan http:server start and boom!

zuohuadong commented 6 years ago

@aftabnaveed Phpize pecl php.ini php-fpm These are obsolete things.Every time I install and expand, I need to understand them all. I have to say that they are too much trouble. Laravel can be like it : https://github.com/nestjs/nest The php server may cause asynchronous problems. Most phpers don't use asynchronous.

franzliedke commented 6 years ago

Booting the framework only once

Laravel will need some more work for this to be production-ready without big hacks, as some components of the framework assume the global context is always a request context (Auth is a good example). But Taylor signaled interest in the idea, and I've started a little bit of work in this area.

mfn commented 6 years ago

Swoole was already mentioned briefly, see https://github.com/swooletw/laravel-swoole

I did a PoC last week and was quite impressed: quick to get it running and raw throughput performance jumped up. In my local dev hammering a simple GET API endpoint with siege got me

(note: the machine is a local dev machine so nothing is tuned for metal performance)

But there are issues as already mentioned here with session/state/context if you're not careful (but in that regard it's not different then any other architecture which doesn't fork a process for each request; simplified said).

halaei commented 6 years ago

One approach that probably works without forcing to rewrite the code in async mode, or even leaning to code that way, would be some architecture like django channels version 1. Using redis queues and blocking pop, I think some can implement it for laravel.

Here is a snapshot from a talk by Andrew Godwin illustrating channels v1. The talk is actually about channels v2, which is completely a different thing.

https://youtu.be/-7taKQnndfo?t=273

franzliedke commented 6 years ago

In general, Laravel's architecture already goes half the way towards making things like PPM a drop-in improvement. Most of this is due to the separation between booting the app (registering and booting of service providers) and request handling (middlewares, controller etc.).

It is currently problematic because, again, some parts of Laravel don't make this separation explicit. Thus, to use PPM, Swoole etc., one has to be aware of nuances and details that many might not know. Of course, this is mostly due to PHP's origin and its default shared-nothing process model, which mostly meant nobody ever had to think about handling multiple requests in one execution context. As long as it's too easy to shoot yourself in the foot (especially in regards to authentication), Laravel should not advertise itself as compatible with these alternative execution model. IMO.

However, if we manage to move the architecture towards something that encourages code according to this strict separation between booting and request handling, then PPM and friends can indeed become drop-in improvements. That is my hope...

aftabnaveed commented 6 years ago

Laravel will need some more work for this to be production-ready without big hacks, as some components of the framework assume the global context is always a request context (Auth is a good example)

@franzliedke I understand and I think most singletons which needs a reset on every request would have that issue. Do you have a repo anywhere for your work? I may also want to have a look.

@mfn I am already using swoole in production and and it is quite good :) , however, I did not mention it here by purpose because swoole requires an additional PHP extension to be installed which I don't think people would like.

I think the idea of having a built in HTTP Server should be separated from this thread. Beyond the Server itself it probably has nothing to do with the bringing in async features to the framework.

zuohuadong commented 6 years ago

@aftabnaveed We used php_redis and fileinfo extensions in open source projects. But a lot of people don't install it. Swoole will be harder to install, which isn't very friendly to many people. But golang nodejs would have made it easier to do this.

Mevrael commented 6 years ago

@aftabnaveed

however, I did not mention it here by purpose because swoole requires an additional PHP extension to be installed which I don't think people would like.

There is absolutely no problem to set up Swoole. You just run few commands in your terminal on Mac/Linux and on Windows you can use Windows Subsystem for Linux. I am a Windows user and got Swoole up and running without any problems.


Apart from that I lean towards Swoole and we might just have another Laravel product developed on the top of Swoole following Laravel philosophy and using it's components/API like we have Lumen already.

We might start developing another small project, Swumen or whatever you would call it, and see if there would be a demand in developing a full-stack framework later, but at least I would like to see Laravel UX and API with Swoole.

I also see that Swoole will change PHP significantly and will become a hype in web dev world next years, and, probably, Swoole will become part of the PHP core itself at some point.

mfn commented 6 years ago

Swumen

😀

sisve commented 6 years ago

I imagine that this idea/feature will progress faster if someone attempts to build it and figure out what in Laravel needs to change. Required extension points can then be PR:ed into the framework until this feature can be built without hacking any core stuff.

Have anyone started?

barryvdh commented 6 years ago

As said before, it's already started here: https://github.com/php-pm/php-pm-httpkernel/blob/master/Bootstraps/Laravel.php for php-pm (https://github.com/php-pm/php-pm)

aftabnaveed commented 6 years ago

@barryvdh PHP-PM a general Process Manager built for Symfony in mind and I already tried it but then went with swoole. The idea of having a laravel http server will make it not only easier for new comers to get started but also put the framework on steroids. This has now become more easy with PSR-7 which ReactPHP supports out of the box.

barryvdh commented 6 years ago

Well yeah, Laravel is also built on Symfony.

But it should give you a starting point is to what to reset.

erycson commented 4 years ago

Example:

<?php

namespace App\Console\Commands;

use Exception;
use React\Http\Response as ReactResponse;
use React\Http\Server as HttpServer;
use React\Socket\Server as SocketServer;
use React\EventLoop\Factory;
use Illuminate\Console\Command;
use Illuminate\Http\Request as LaravelRequest;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

class ReactRequestException extends Exception
{
    protected $request = null;

    public function __construct(Exception $e, $request)
    {
        parent::__construct($e->getMessage(), $e->getCode(), $e->getPrevious());
        $this->request = $request;
    }

    public function getRequest()
    {
        return $this->request;
    }

    public function hasRequest()
    {
        return !is_null($this->request);
    }
}

class Serve extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'serve {host=0.0.0.0} {port=8000}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $host = $this->argument('host');
        $port = $this->argument('port');

        $factory = new Psr17Factory;
        $psrHttpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
        $httpFoundationFactory = new HttpFoundationFactory;
        $kernel = app()->make(\Illuminate\Contracts\Http\Kernel::class);

        $loop = Factory::create();
        $socket = new SocketServer("{$host}:{$port}", $loop);
        $server = new HttpServer(function (ServerRequestInterface $psrRequest) use (
            $kernel,
            $psrHttpFactory,
            $httpFoundationFactory
        ) {
            $request = null;
            try {
                $request = LaravelRequest::createFromBase(
                    $httpFoundationFactory->createRequest($psrRequest)
                );
                $response = $kernel->handle($request);
                $kernel->terminate($request, $response);

                return $psrHttpFactory->createResponse($response);
            } catch (\Exception $e) {
                throw new ReactRequestException($e, $request);
            }
        });
        $server->on('error', function (Exception $e) use ($psrHttpFactory) {
            $handler = resolve(\App\Exceptions\Handler::class);
            $handler->report($e);

            if ($e instanceof ReactRequestException &&  $e->hasRequest()) {
                return $psrHttpFactory->createResponse(
                    $handler->render($e->getRequest(), $e)
                );
            } else {
                return new ReactResponse(500);
            }
        });
        $server->listen($socket);

        $this->info("Server started at {$host}:{$port}");

        app()->singleton('loop', $loop);
        $loop->run();
    }

    protected function getArguments()
    {
        return array(
            ['host', 'h', InputOption::VALUE_OPTIONAL, 'The host address to serve the application on.', '0.0.0.0'],
            ['port', 'p', InputOption::VALUE_OPTIONAL, 'The port to serve the application on.', 8000],
        );
    }
}

Packages:

Execute: php artisan serve

shahidkarimi commented 4 years ago

This is a revolutionary stuff.

Fusty commented 3 years ago

Very cool stuff @erycson !!

Example:

<?php

namespace App\Console\Commands;

use Exception;
use React\Http\Response as ReactResponse;
use React\Http\Server as HttpServer;
use React\Socket\Server as SocketServer;
use React\EventLoop\Factory;
use Illuminate\Console\Command;
use Illuminate\Http\Request as LaravelRequest;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

class ReactRequestException extends Exception
{
    protected $request = null;

    public function __construct(Exception $e, $request)
    {
        parent::__construct($e->getMessage(), $e->getCode(), $e->getPrevious());
        $this->request = $request;
    }

    public function getRequest()
    {
        return $this->request;
    }

    public function hasRequest()
    {
        return !is_null($this->request);
    }
}

class Serve extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'serve {host=0.0.0.0} {port=8000}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $host = $this->argument('host');
        $port = $this->argument('port');

        $factory = new Psr17Factory;
        $psrHttpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
        $httpFoundationFactory = new HttpFoundationFactory;
        $kernel = app()->make(\Illuminate\Contracts\Http\Kernel::class);

        $loop = Factory::create();
        $socket = new SocketServer("{$host}:{$port}", $loop);
        $server = new HttpServer(function (ServerRequestInterface $psrRequest) use (
            $kernel,
            $psrHttpFactory,
            $httpFoundationFactory
        ) {
            $request = null;
            try {
                $request = LaravelRequest::createFromBase(
                    $httpFoundationFactory->createRequest($psrRequest)
                );
                $response = $kernel->handle($request);
                $kernel->terminate($request, $response);

                return $psrHttpFactory->createResponse($response);
            } catch (\Exception $e) {
                throw new ReactRequestException($e, $request);
            }
        });
        $server->on('error', function (Exception $e) use ($psrHttpFactory) {
            $handler = resolve(\App\Exceptions\Handler::class);
            $handler->report($e);

            if ($e instanceof ReactRequestException &&  $e->hasRequest()) {
                return $psrHttpFactory->createResponse(
                    $handler->render($e->getRequest(), $e)
                );
            } else {
                return new ReactResponse(500);
            }
        });
        $server->listen($socket);

        $this->info("Server started at {$host}:{$port}");

        app()->singleton('loop', $loop);
        $loop->run();
    }

    protected function getArguments()
    {
        return array(
            ['host', 'h', InputOption::VALUE_OPTIONAL, 'The host address to serve the application on.', '0.0.0.0'],
            ['port', 'p', InputOption::VALUE_OPTIONAL, 'The port to serve the application on.', 8000],
        );
    }
}

Packages:

  • nyholm/psr7 ^1.2
  • react/http ^0.8.6
  • react/react ^1.0
  • symfony/psr-http-message-bridge ^2.0

Execute: php artisan serve

If you add this before you call $kernel->terminate, you can handle static files normally served from the /public/ directory.

if($response->status() === 404) {
    $path = public_path($request->decodedPath());

    if(file_exists($path)) {
        $response = response()->file($path);
    }
}

NOTE: You should probably ensure $path is a subdirectory of /public/ and someone is not ../../ -ing their way out of /public/.

EDIT: I've updated @erycson's post to the newer ReactPHP EventLoop syntax (removes Factory::create for the event loop).

<?php

namespace App\Console\Commands;

use Exception;
use React\Http\Message\Response as ReactResponse;
use React\Http\Server as HttpServer;
use React\Socket\Server as SocketServer;
use Illuminate\Console\Command;
use Illuminate\Http\Request as LaravelRequest;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

class ReactRequestException extends Exception
{
    protected $request = null;

    public function __construct(Exception $e, $request)
    {
        parent::__construct($e->getMessage(), $e->getCode(), $e->getPrevious());
        $this->request = $request;
    }

    public function getRequest()
    {
        return $this->request;
    }

    public function hasRequest()
    {
        return !is_null($this->request);
    }
}

class Serve extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'serve {host=0.0.0.0} {port=8080}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $host = $this->argument('host');
        $port = $this->argument('port');

        $factory = new Psr17Factory;
        $psrHttpFactory = new PsrHttpFactory($factory, $factory, $factory, $factory);
        $httpFoundationFactory = new HttpFoundationFactory;
        $kernel = app()->make(\Illuminate\Contracts\Http\Kernel::class);

        $socket = new SocketServer("{$host}:{$port}");
        $server = new HttpServer(function (ServerRequestInterface $psrRequest) use (
            $kernel,
            $psrHttpFactory,
            $httpFoundationFactory
        ) {
            $request = null;
            try {
                $request = LaravelRequest::createFromBase(
                    $httpFoundationFactory->createRequest($psrRequest)
                );

                $response = $kernel->handle($request);

                if($response->status() === 404) {
                    $path = public_path($request->decodedPath());

                    if(file_exists($path)) {
                        $response = response()->file($path);
                    }
                }

                $kernel->terminate($request, $response);

                return $psrHttpFactory->createResponse($response);
            } catch (\Exception $e) {
                throw new ReactRequestException($e, $request);
            }
        });
        $server->on('error', function (Exception $e) use ($psrHttpFactory) {
            $handler = resolve(\App\Exceptions\Handler::class);
            $handler->report($e);

            if ($e instanceof ReactRequestException &&  $e->hasRequest()) {
                return $psrHttpFactory->createResponse(
                    $handler->render($e->getRequest(), $e)
                );
            } else {
                return new ReactResponse(500);
            }
        });

        $server->listen($socket);

        $this->info("Server started at {$host}:{$port}");
    }

    protected function getArguments()
    {
        return array(
            ['host', 'h', InputOption::VALUE_OPTIONAL, 'The host address to serve the application on.', '0.0.0.0'],
            ['port', 'p', InputOption::VALUE_OPTIONAL, 'The port to serve the application on.', 8000],
        );
    }
}

Use the static methods on the React\EventLoop\Loop class to access the global event loop.

For instance

Route::get('/tick', function(){
    $timer = Loop::addPeriodicTimer(0.1, function () {
        echo 'Tickin away after the response is sent!' . PHP_EOL;
    });

    Loop::addTimer(1.0, function () use ($timer) {
        Loop::cancelTimer($timer);
        echo 'Done' . PHP_EOL;
    });
    return 'Ticks Started!';
});
alcarazolabs commented 3 years ago

I know a guy that used ReactPHP and laravel to dump 300k rows from database into a csv file.. and not delays, time outs.. was fast.. using laravel jobs