PHPFastCGI / FastCGIDaemon

A FastCGI daemon written in PHP.
http://phpfastcgi.github.io/
MIT License
331 stars 16 forks source link

Phalcon Adapter #18

Open Jurigag opened 8 years ago

Jurigag commented 8 years ago

Im currently using phalcon framework, how to implement adapter for your phpfastcgi ? Any hints ? Creating something similar like for slim/symfony/silex is enough ?

AndrewCarterUK commented 8 years ago

Hello,

I've just spend some time looking into the phalcon framework (I've never used it before). Although it is possible to use PHPFastCGI with this phalcon, it is more difficult than with other frameworks.

The main reason for this is that the Application::handle() method isn't designed to be run over multiple request cycles. This line (and the one below it) shows that it sends the headers and cookies on each call to the method. This could be a problem when running under PHPFastCGI as trying to dispatch the headers and cookies like this will have no meaning in a command line environment (and it might face issues trying to redispatch them on the next request).

That said, I think that it might be possible to hack the application class into providing a 'request in, response out' style access point (without the framework attempting any output in between or any other side effects). I think this could be done by replacing the request and response objects in the container on each new request and creating a method similar to Application::handle() but without the side effects.

Once this is done it would be a case of implementing the Phalcon HTTP request and response interfaces and creating a service which converted objects from these implementations to PSR-7 or Symfony Foundation objects.

If you do want to have a crack at this keep me updated and I'll see what I can do to help. I may even have a go at this myself. The only warning I would give is that the layout of the framework suggests the developers never considered or anticipated this kind of use case - tread carefully! :)

All the best,

Andrew

Jurigag commented 8 years ago

Cant i possibly create my custom PhalconRequest class which will be implemanting phalcons' RequestInterface and set there all values provided from PSR7 http request/symfony request and then replace/set phalcons' request in di on this object of my custom request ? Maybe it could work in that way ?

AndrewCarterUK commented 8 years ago

As a correction to my last comment, you would only need to implement the phalcon request interface (not the response interface).

And yes, that's basically the process - but you have to be careful of the Application::handle() method. The reason for this is that this method will try and actually output response information when you call it (rather than just providing the response object cleanly). I'd recommend studying it to figure out what it does so that you can create a new method which cleanly accepts a request and returns a response without any side effects.

Jurigag commented 8 years ago

Well i implement PhalconRequest, and then create new Symfony Response with content from phalcons' application->handle->getContent(). But for some reason router is not working at all, all routes are handling by notFound handler.

There is router code - https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/router.zep

There is my phalcon request code:

<?php
/**
 * Created by PhpStorm.
 * User: Wojtek
 * Date: 2015-11-30
 * Time: 01:59
 */

namespace PHPFastCGI\Adapter\Phalcon;

use Phalcon\Http\RequestInterface;
use Symfony\Component\HttpFoundation\Request;

class PhalconRequest implements RequestInterface
{
    /**
     * @var Request
     */
    private $symfonyFoundtationRequest;

    /**
     * PHP 5 allows developers to declare constructor methods for classes.
     * Classes which have a constructor method call this method on each newly-created object,
     * so it is suitable for any initialization that the object may need before it is used.
     *
     * Note: Parent constructors are not called implicitly if the child class defines a constructor.
     * In order to run a parent constructor, a call to parent::__construct() within the child constructor is required.
     *
     * param [ mixed $args [, $... ]]
     * @return void
     * @link http://php.net/manual/en/language.oop5.decon.php
     */
    function __construct($symfonyFoundationRequest)
    {
        $this->symfonyFoundtationRequest = $symfonyFoundationRequest;
    }

    /**
     * Gets a variable from the $_REQUEST superglobal applying filters if needed
     *
     * @param string $name
     * @param string|array $filters
     * @param mixed $defaultValue
     * @return mixed
     */
    public function get($name = null, $filters = null, $defaultValue = null)
    {
        return $this->symfonyFoundtationRequest->attributes->filter($name, $filters, $defaultValue);
    }

    /**
     * Gets a variable from the $_POST superglobal applying filters if needed
     *
     * @param string $name
     * @param string|array $filters
     * @param mixed $defaultValue
     * @return mixed
     */
    public function getPost($name = null, $filters = null, $defaultValue = null)
    {
        return $this->symfonyFoundtationRequest->request->filter($name, $filters, $defaultValue);
    }

    /**
     * Gets variable from $_GET superglobal applying filters if needed
     *
     * @param string $name
     * @param string|array $filters
     * @param mixed $defaultValue
     * @return mixed
     */
    public function getQuery($name = null, $filters = null, $defaultValue = null)
    {
        return $this->symfonyFoundtationRequest->query->filter($name, $filters, $defaultValue);
    }

    /**
     * Gets variable from $_SERVER superglobal
     *
     * @param string $name
     * @return mixed
     */
    public function getServer($name)
    {
        return $this->symfonyFoundtationRequest->server->get($name);
    }

    /**
     * Checks whether $_REQUEST superglobal has certain index
     *
     * @param string $name
     * @return bool
     */
    public function has($name)
    {
        return $this->symfonyFoundtationRequest->attributes->has($name);
    }

    /**
     * Checks whether $_POST superglobal has certain index
     *
     * @param string $name
     * @return bool
     */
    public function hasPost($name)
    {
        return $this->symfonyFoundtationRequest->request->has($name);
    }

    /**
     * Checks whether the PUT data has certain index
     *
     * @param string $name
     * @return bool
     */
    public function hasPut($name)
    {
        $put = $this->symfonyFoundtationRequest->getContent(true);
        if (in_array($name, $put)) {
            return true;;
        } else {
            return false;
        }
    }

    /**
     * Checks whether $_GET superglobal has certain index
     *
     * @param string $name
     * @return bool
     */
    public function hasQuery($name)
    {
        return $this->symfonyFoundtationRequest->query->has($name);
    }

    /**
     * Checks whether $_SERVER superglobal has certain index
     *
     * @param string $name
     * @return bool
     */
    public function hasServer($name)
    {
        return $this->symfonyFoundtationRequest->server->has($name);
    }

    /**
     * Gets HTTP header from request data
     *
     * @param string $header
     * @return string
     */
    public function getHeader($header)
    {
        return $this->symfonyFoundtationRequest->headers->get($header);
    }

    /**
     * Gets HTTP schema (http/https)
     *
     * @return string
     */
    public function getScheme()
    {
        return $this->symfonyFoundtationRequest->getScheme();
    }

    /**
     * Checks whether request has been made using ajax. Checks if $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
     *
     * @return bool
     */
    public function isAjax()
    {
        return $this->symfonyFoundtationRequest->isXmlHttpRequest();
    }

    /**
     * Checks whether request has been made using SOAP
     *
     * @return bool
     */
    public function isSoapRequested()
    {
        if ($this->symfonyFoundtationRequest->server->has('HTTP_SOAPACTION')) {
            return true;
        } else {
            $contentType = $this->symfonyFoundtationRequest->getContentType();
            if (!empty($contentType) && $contentType == "application/soap+xml") {
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * Checks whether request has been made using any secure layer
     *
     * @return bool
     */
    public function isSecureRequest()
    {
        return $this->symfonyFoundtationRequest->isSecure();
    }

    /**
     * Gets HTTP raws request body
     *
     * @return string
     */
    public function getRawBody()
    {
        return $this->symfonyFoundtationRequest->getContent();
    }

    /**
     * Gets active server address IP
     *
     * @return string
     */
    public function getServerAddress()
    {
        if ($this->symfonyFoundtationRequest->server->has("SERVER_ADDR")) {
            return $this->symfonyFoundtationRequest->server->get("SERVER_ADDR");
        } else {
            return gethostbyname("localhost");
        }
    }

    /**
     * Gets active server name
     *
     * @return string
     */
    public function getServerName()
    {
        if ($this->symfonyFoundtationRequest->server->has("SERVER_NAME")) {
            return $this->symfonyFoundtationRequest->server->get("SERVER_NAME");
        } else {
            return "localhost";
        }
    }

    /**
     * Gets information about schema, host and port used by the request
     *
     * @return string
     */
    public function getHttpHost()
    {
        return $this->symfonyFoundtationRequest->getHttpHost();
    }

    /**
     * Gets most possibly client IPv4 Address. This methods search in $_SERVER['REMOTE_ADDR'] and optionally in $_SERVER['HTTP_X_FORWARDED_FOR']
     *
     * @param bool $trustForwardedHeader
     * @return string
     */
    public function getClientAddress($trustForwardedHeader = false)
    {
        return $this->symfonyFoundtationRequest->getClientIp();
    }

    /**
     * Gets HTTP method which request has been made
     *
     * @return string
     */
    public function getMethod()
    {
        return $this->symfonyFoundtationRequest->getMethod();
    }

    /**
     * Gets HTTP user agent used to made the request
     *
     * @return string
     */
    public function getUserAgent()
    {
        if ($this->symfonyFoundtationRequest->server->has("HTTP_USER_AGENT")) {
            return $this->symfonyFoundtationRequest->server->get("HTTP_USER_AGENT");
        } else {
            return "localhost";
        }
    }

    /**
     * Check if HTTP method match any of the passed methods
     *
     * @param string|array $methods
     * @param bool $strict
     * @return boolean
     */
    public function isMethod($methods, $strict = false)
    {
        if (is_array($methods)) {
            foreach ($methods as $method) {
                if (!$this->symfonyFoundtationRequest->isMethod($method)) {
                    return false;
                }
            }
            return true;
        } else {
            return $this->symfonyFoundtationRequest->isMethod($methods);
        }
    }

    /**
     * Checks whether HTTP method is POST. if $_SERVER['REQUEST_METHOD']=='POST'
     *
     * @return bool
     */
    public function isPost()
    {
        return $this->symfonyFoundtationRequest->isMethod('POST');
    }

    /**
     * Checks whether HTTP method is GET. if $_SERVER['REQUEST_METHOD']=='GET'
     *
     * @return bool
     */
    public function isGet()
    {
        return $this->symfonyFoundtationRequest->isMethod('GET');
    }

    /**
     * Checks whether HTTP method is PUT. if $_SERVER['REQUEST_METHOD']=='PUT'
     *
     * @return bool
     */
    public function isPut()
    {
        return $this->symfonyFoundtationRequest->isMethod('PUT');
    }

    /**
     * Checks whether HTTP method is HEAD. if $_SERVER['REQUEST_METHOD']=='HEAD'
     *
     * @return bool
     */
    public function isHead()
    {
        return $this->symfonyFoundtationRequest->isMethod('HEAD');
    }

    /**
     * Checks whether HTTP method is DELETE. if $_SERVER['REQUEST_METHOD']=='DELETE'
     *
     * @return bool
     */
    public function isDelete()
    {
        return $this->symfonyFoundtationRequest->isMethod('DELETE');
    }

    /**
     * Checks whether HTTP method is OPTIONS. if $_SERVER['REQUEST_METHOD']=='OPTIONS'
     *
     * @return bool
     */
    public function isOptions()
    {
        return $this->symfonyFoundtationRequest->isMethod('OPTIONS');
    }

    /**
     * Checks whether request include attached files
     *
     * @param boolean $onlySuccessful
     * @return boolean
     */
    public function hasFiles($onlySuccessful = false)
    {
        return $this->symfonyFoundtationRequest->files->count() > 0;
    }

    /**
     * Gets attached files as Phalcon\Http\Request\FileInterface compatible instances
     *
     * @param bool $onlySuccessful
     * @return \Phalcon\Http\Request\FileInterface
     */
    public function getUploadedFiles($onlySuccessful = false)
    {
        return $this->symfonyFoundtationRequest->files->all();
    }

    /**
     * Gets web page that refers active request. ie: http://www.google.com
     *
     * @return string
     */
    public function getHTTPReferer()
    {
        if ($this->symfonyFoundtationRequest->server->has("HTTP_REFERER")) {
            return $this->symfonyFoundtationRequest->server->get("HTTP_REFERER");
        } else {
            return "";
        }
    }

    /**
     * Gets array with mime/types and their quality accepted by the browser/client from $_SERVER['HTTP_ACCEPT']
     *
     * @return array
     */
    public function getAcceptableContent()
    {
        return $this->symfonyFoundtationRequest->getAcceptableContentTypes();
    }

    /**
     * Gets best mime/type accepted by the browser/client from $_SERVER['HTTP_ACCEPT']
     *
     * @return string
     */
    public function getBestAccept()
    {
        return $this->symfonyFoundtationRequest->headers->get('ACCEPT');
    }

    /**
     * Gets charsets array and their quality accepted by the browser/client from $_SERVER['HTTP_ACCEPT_CHARSET']
     *
     * @return array
     */
    public function getClientCharsets()
    {
        return $this->symfonyFoundtationRequest->getCharsets();
    }

    /**
     * Gets best charset accepted by the browser/client from $_SERVER['HTTP_ACCEPT_CHARSET']
     *
     * @return string
     */
    public function getBestCharset()
    {
        return $this->symfonyFoundtationRequest->headers->get('Accept-Charset');
    }

    /**
     * Gets languages array and their quality accepted by the browser/client from _SERVER['HTTP_ACCEPT_LANGUAGE']
     *
     * @return array
     */
    public function getLanguages()
    {
        return $this->symfonyFoundtationRequest->getLanguages();
    }

    /**
     * Gets best language accepted by the browser/client from $_SERVER['HTTP_ACCEPT_LANGUAGE']
     *
     * @return string
     */
    public function getBestLanguage()
    {
        return $this->symfonyFoundtationRequest->headers->get('Accept-Language');
    }

    /**
     * Gets auth info accepted by the browser/client from $_SERVER['PHP_AUTH_USER']
     *
     * @return array
     */
    public function getBasicAuth()
    {
        if ($this->symfonyFoundtationRequest->server->has("PHP_AUTH_USER") && $this->symfonyFoundtationRequest->server->has("PHP_AUTH_PW")) {
            $auth = [];
            $auth['username'] = $this->symfonyFoundtationRequest->server->get("PHP_AUTH_USER");
            $auth['password'] = $this->symfonyFoundtationRequest->server->get("PHP_AUTH_PW");
            return $auth;
        }
        return null;
    }

    public function getUri()
    {
        return $this->symfonyFoundtationRequest->getUri();
    }

    /**
     * Gets auth info accepted by the browser/client from $_SERVER['PHP_AUTH_DIGEST']
     *
     * @return array
     */
    public function getDigestAuth()
    {
        $auth = [];
        if ($this->symfonyFoundtationRequest->server->has("PHP_AUTH_DIGEST")) {
            $digest = $this->symfonyFoundtationRequest->server->get("PHP_AUTH_DIGEST");
            $matches = [];
            if (!preg_match_all("#(\\w+)=(['\"]?)([^'\" ,]+)\\2#", $digest, $matches, 2)) {
                return $auth;
            } else {
                if(is_array($matches)){
                    foreach($matches as $match){
                        $auth[$match[1]] = $match[3];
                    }
                }
            }
        }
        return $auth;
    }

}

And here AppWrapper:


<?php
/**
 * Created by PhpStorm.
 * User: Wojtek
 * Date: 2015-11-30
 * Time: 01:03
 */

namespace PHPFastCGI\Adapter\Phalcon;

use Phalcon\Di;
use Phalcon\Mvc\Application;
use Phalcon\Mvc\Micro;
use PHPFastCGI\FastCGIDaemon\Http\Request;
use PHPFastCGI\FastCGIDaemon\Http\RequestInterface;
use PHPFastCGI\FastCGIDaemon\KernelInterface;
use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse;
use Symfony\Component\HttpFoundation\Response;

class AppWrapper implements KernelInterface
{
    /**
     * @var Micro|Application
     */
    private $application;

    public function __construct($application)
    {
        $this->application = $application;
    }

    /**
     * Handles a request and returns a response.
     *
     * @param RequestInterface $request FastCGI HTTP request object
     *
     * @return ResponseInterface|HttpFoundationResponse|Request HTTP response message
     */

    public function handleRequest(RequestInterface $request)
    {
        require_once('PhalconRequest.php');
        $phalconRequest = new PhalconRequest($request->getHttpFoundationRequest());
        $this->application->request=$phalconRequest;
        $this->application->handle();
        $phalconResponse = $this->application->response;
        $response = new Response($phalconResponse->getContent(),'200');
        return $response;
    }

}

Well i guess the best would be just logging those conditions etc from phalcons' router and check why its not matching any route.

AndrewCarterUK commented 8 years ago

Try passing the request uri to $this->application->handle()

Jurigag commented 8 years ago

Checked it already, still the same.

Jurigag commented 8 years ago

Okay, found it, i have to set request in this way:

$this->application->di->setShared('request', function () use ($phalconRequest) {
            return $phalconRequest;
        });;
Jurigag commented 8 years ago

Now its working fine, but there is one problem - i dont have any response. I just got output in console application.

AndrewCarterUK commented 8 years ago

Instead of:

        $this->application->handle();
        $phalconResponse = $this->application->response;

Try:

    $phalconResponse = $this->application->handle();
Jurigag commented 8 years ago

Well after setting content of Symfony Response, and after var_dump($response) its saying content is empty. Weird.

AndrewCarterUK commented 8 years ago

Okay, I'll have a look at making an adapter based off the code you've got here. Let me know if you have any progress in the mean time.

I'm at SymfonyCon in Paris this week presenting PHPFastCGI, so I should get some time on the flight to investigate.

Jurigag commented 8 years ago

Well i got some progress. On micro app its working pretty much fine. On low machine(1 core, 512 mb ram) i got like +50 req on select from db and returning json). Now im working on full application with multi module to make it work. In this case for some reason dispatcher is stuck in same module for all the time.

I had to comment this line in phalcons' micro app: https://github.com/phalcon/cphalcon/blob/master/phalcon/mvc/micro.zep#L883

I guess we have to do the same for full application.

Also, after setting httpFoundatonRequest we have to set $_SERVER['REQUEST_URI'] cuz phalcon use it. And in router config we have to set $router->setUriSource(Router::URI_SOURCE_SERVER_REQUEST_URI);

The wrapper looks like this currently:

require_once('PhalconRequest.php');
        $phalconRequest = new PhalconRequest();
        $phalconRequest->setSymfonyFoundtationRequest($request->getHttpFoundationRequest());
        foreach ($request->getHttpFoundationRequest()->cookies->all() as $key => $value){
            if($this->application->cookies->isUsingEncryption()){
                $this->application->cookies->set($key, $value,0,"/",true);
            }
            else{
                $this->application->cookies->set($key, $value,0,"projekt.jurigag.me");
            }
        }
        $this->application->di->set('request', function () use ($phalconRequest) {
            $request = clone $phalconRequest;
            return $request;
        });
        $this->application->request=$phalconRequest;
        $response = new Response();
        $phalconResponse =  $this->application->handle();
        if(is_object($phalconResponse)) {
            $content = $phalconResponse->getContent();
            $response->setContent($content);
        }
        else{
            $response->setContent($phalconResponse);
        }
        $statusCode=$this->application->response->getStatusCode();
        if(!empty($statusCode)){
            $response->setStatusCode((int)$statusCode);
        }
        else{
            $response->setStatusCode(200);
        }
        $response->headers->set('Content-Type', 'text/html');
        $this->application->response=new \Phalcon\Http\Response();
        $this->application->di->remove('request');
        return $response;
AndrewCarterUK commented 8 years ago

Glad you've made some progress.

I'm going to rename this issue to 'Phalcon Adapter', because it appears to me that the main issue is that Phalcon isn't a simple request/response framework - and thus the adapter has to be much more complicated than with the other frameworks.

I plan on creating a couple of issues with the Phalcon team which would hopefully make the framework much more suitable for this purpose.

Jurigag commented 8 years ago

Fine, i spend almost all whole day on it. I give up after this weird multi-module problem. Like if i accessed route from one module, then i couldnt acces routes from other modules at all(returning 404).

I guess this caused by maybe some dispatcher/router problem but i was checking it all times and it was working fine.