Tuesday, May 7, 2013

Windows, NginX, MySQL, and PHP

Introduction

A while back I setup a LEMP server (Linux, NginX, MySQL, and PHP) using Virtual Box. This is fine for testing out how a live server might behave, or even for using as a virtual server if you don't have the dedicated hardware to host a server. However, for a server in active development it's a huge inconvenience as everytime I want to test out a change, no matter how small, I was forced to push the changes to the virtual server. To get around this, I'm going to setup a development server which can run in the Windows environment I normally use. I'm also going to setup the XDebug plugin. This will allow me to debug php code without using the "traditional" console/echo solution. I'm unsure if XDebug would work with my virtual server setup, but I do know it works with this development setup.

Required Software

In order to have the server automatically, I wrote a small C# wrapper application which runs as a service. The service is able to startup and shutdown the PHP-FPM and NginX applications as needed. This isn't a necessary step, but really is convenient.

Install MySQL Software

This step is fairly self-explainatory, just run the MySql installer and install the MySQL web server and MySQL Workbench. You can install other tools as needed, or come back later to modify your installation. I'm unsure if the MySQL server needs to be the 32-bit version or if you can use the 64-bit version. NginX and PHP are currently 32-bit only for Windows. I modified the specific configuration options to minimize memory usage. I don't really need to use the MySQL database all the time, and when I do it will be for low-demand development work. I also included the same bind-address directive to limit access (in addition to user account access limits). This time, I can limit access directly to the localhost rather than have to delegate to a different machine.

Configuring NginX and PHP

The first step is to unzip the downloads. Configuring the two is similar to configuring in Linux. Here is the nginx.conf I'm using:

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  text/html;

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    gzip  on;
 gzip_static on;
 gzip_vary on;
 gzip_disable "msie6";
 
 gzip_types text/plain text/html text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
 gzip_comp_level 6;
 gzip_http_version 1.1;

    server {
        listen       80;
  listen [::]:80 default ipv6only=on; ## listen for ipv6
  
  root "C:/Users/Andrew/Documents/Eclipse/main/";
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
  
  index index.html index.html index.php;

        location / {
   deny all;
        }
  
  location /allowed_page {
   allow all;
      try_files $uri $uri/ /index.html;
  }
  
  # block access to svn directories
  location ~/\.svn {
   deny all;
  }
  
  # block access to git directories
  location ~/\.git {
   deny all;
  }
  
  # block eclipse project and workspace stuff
  location ~/\.settings {
   deny all;
  }
  
  location ~/\.buildpath {
   deny all;
  }
  
  location ~/\.project {
   deny all;
  }
  
  location ~/\.metadata {
   deny all;
  }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        #error_page   500 502 503 504  /50x.html;
        #location = /50x.html {
        #    root   html;
        #}
        
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        location ~ \.php$ {
   try_files  $uri =404;
            fastcgi_pass   127.0.0.1:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
            include        fastcgi_params;
        }
    }
}

There are a few notable modifications from the default config file provided by NginX, notably the PHP handling has the fastcgi_param set correctly (the default setting is meaningless). I also by default denied access to my Eclipse repository, and explicitly allowed certain directories I wanted to allow the server access to. Alternatively, I could only allow 127.0.0.1 access instead of allow all.

Also note that according to the NginX release notes, NginX for Windows currently does not properly handle multiple worker processes (only 1 actually does any work). Since there will be very few people using this server, this is fine.

To configure PHP-CGI with XDebug support, go to the PHP directory and copy/rename php.ini-development to php.ini. These are the default configs which help with developing PHP code, which is close to what I want.

MySQL modifications

There are 2 modifications that need to be changed to enable the MySQL PDO extension. Do a search for the following lines and uncomment them.

; uncomment this line to ensure PHP can find the extensions
extension_dir = "ext"

; uncomment this line to enable the MySQL PDO extension
extension=php_pdo_mysql.dll

XDebug modifications

The first step is to move the XDebug dll to the php ext folder (actually, you can probably move it anywhere but this is the logical place to put it). Next, edit the php.ini file and add the following code:

; I don't know if this has to be in the [PHP] section or not, I put it right under the other extensions
zend_extension="C:/Programs/php/php-5.4.13-Win32-VC9-x86/ext/php_xdebug-2.2.2-5.4-vc9.dll"

; note: make sure you add this so it's not in the middle of another section.
; Adding to the end is a good way to do so
[xdebug]
xdebug.remote_enable=on
xdebug.remote_host=localhost
xdebug.remote_port=9001
xdebug.remote_handler="dbgp"

A key point to note is that the default PHP-FCGI port is 9000, which is what XDebug's install instructions say to use. The easy fix to this is simply move XDebug's port to 9001.

Starting and Stopping the Server

Starting the server is pretty easy. Start NginX by running the main executable, nginx.exe. Start the PHP-FCGI server is simple, too. Run php-cgi.exe with the target IP address and port (as configured in nginx.conf).

php-cgi.exe -b 127.0.0.1:9000 -c ./php.ini

To stop the NginX server, use the nginx -s quit for a graceful shutdown. See the NginX documentation for more information.

Windows Service Wrapper

To keep the server running in the background, I'm going to use a Windows Service wrapper application. I'm basically following the MSDN tutorial. I couldn't find any easy way to create a Windows service using VC++ with Visual Studio 2010/2012 (the two versions I have available) so I settled with C#. I don't need special logging for just the service wrapper so I ignored the steps about adding an EventLog component. Here's the code for the main service class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace NginXService
{
    public partial class NginXService : ServiceBase
    {
        private string nginx_path;
        private string php_path;
        private string php_cfg;

        public NginXService()
        {
            InitializeComponent();
            nginx_path = ConfigurationManager.AppSettings.Get("nginx_path");
            php_path = ConfigurationManager.AppSettings.Get("php_path");
            php_cfg = ConfigurationManager.AppSettings.Get("php_cfg");
        }

        private bool nginxRunning()
        {
            Process[] processlist = Process.GetProcesses();
            foreach (Process p in processlist)
            {
                if (p.ProcessName.ToLower().Equals("nginx"))
                {
                    return true;
                }
            }
            return false;
        }

        private Process getPHPProcess()
        {
            Process[] processlist = Process.GetProcesses();
            foreach (Process p in processlist)
            {
                if (p.ProcessName.ToLower().Equals("php-cgi"))
                {
                    return p;
                }
            }
            return null;
        }

        protected override void OnStart(string[] args)
        {
            start();
        }

        protected override void OnShutdown()
        {
            quit();
        }

        protected override void OnStop()
        {
            quit();
        }

        public void quit()
        {
            if (nginxRunning())
            {
                ProcessStartInfo info = new ProcessStartInfo(nginx_path + "nginx.exe", "-s quit");
                info.WorkingDirectory = nginx_path;
                Process.Start(info);
            }
            Process php = getPHPProcess();
            if (php != null)
            {
                php.Kill();
            }
        }

        public void start()
        {
            if (!nginxRunning())
            {
                ProcessStartInfo info = new ProcessStartInfo(nginx_path + "nginx.exe");
                info.WorkingDirectory = nginx_path;
                Process.Start(info);
            }
            Process php = getPHPProcess();
            if (php == null)
            {
                ProcessStartInfo info = new ProcessStartInfo(php_path + "php-cgi.exe", "-b 127.0.0.1:9000 -c \"" + php_cfg + "\"");
                info.WorkingDirectory = php_path;
                Process.Start(info);
            }
        }
    }
}

To help generalize the wrapper application I added 3 appSettings properties. This allows the wrapper to handle various installation paths without having to recompile the application.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <appSettings>
        <add key="nginx_path" value="C:\\Programs\\nginx\\nginx-1.3.15\\" />
        <add key="php_path" value="C:\\Programs\\php\\php-5.4.13-Win32-VC9-x86\\"/>
        <add key="php_cfg" value="C:\\Programs\\php\\php-5.4.13-Win32-VC9-x86\\php.ini"/>
        <add key="ClientSettingsProvider.ServiceUri" value="" />
    </appSettings>
    <system.web>
        <membership defaultProvider="ClientAuthenticationMembershipProvider">
            <providers>
                <add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
            </providers>
        </membership>
        <roleManager defaultProvider="ClientRoleProvider" enabled="true">
            <providers>
                <add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
            </providers>
        </roleManager>
    </system.web>
</configuration>

This service handles starting and stopping the PHP-FCGI and NginX processes. Note that MySQL is not handled as the MySQL installer created a separate service which handles this. The code could probably use with some more error handling and checking, but it is usable enough for my needs.

Eclipse PDT Setup

Now that we have a server setup, we need to setup the Eclipse PDT plugin. Open up the Eclipse workspace properties (Windows > Preferences), go to the PHP > PHP Servers page. Add a new web server with the appropriate parameters.

Next go to the PHP > Debug page. Change the PHP Debugger to XDebug and make sure the correct server is selected. Click Configure... and change the XDebug port to match the php.ini port set for XDebug.

Conclusion

And there we go! I now have a local development server running on my Windows system. In addition to this, I now have the ability to use Eclipse's PDT debugging capabilities, which is closer to the traditional debugger interface I'm used to and will make debugging my PHP code much easier.

4 comments :

  1. Well, your blog is really good which have useful information regarding PHP development services. Beginners should read your posts because it has lots of good tips which will useful for their career.
    hire a ASP.NET developer

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. There is also WT-NMP - Windows Nginx MySql Php stack:

    https://sourceforge.net/p/wtnmp/wiki/Home/

    - TRULLY PORTABLE, you can zip it and take it with you, or you can move it to a different location on your filesystem.
    - Easy to upgrade: Download / Unzip / Overwrite!

    ReplyDelete
  4. Interesting project, I'll take a look into what it can do.

    ReplyDelete