Bridging Python And PHP
tags: dev php python xmlrpc
11 Jan 2009 10:48
Imagine you have a PHP-based application (like Wikidot). Now, you want to extend it using Python. Through all ways to do it, I'll show you how to achieve this using XML-RPC protocol.
Background
XML-RPC is a client-server protocol for remote procedure call.
On server this works like getting a bunch of functions from your application and exporting it with HTTP.
On client this works like connecting to a XML-RPC server, finding out what function it delivers and constructing a so called server proxy — an object having a method for every function exported by an XML-RPC server.
Calling the methods of the server proxy connects to the server using HTTP, passes arguments and transport the result back to the client. So basically this works AS you have a remote located object locally available.
The data encoding between client and server is defined in XML-RPC specification and is a language based on XML (but you actually never touch it, the XML is converted to objects by libraries).
Overview
We want to run an XML-RPC server exposing a class in PHP and an XML-RPC client in Python to communicate with the XML-RPC server.
Traditionally we would need to have an HTTP server for the PHP XML-RPC server, because HTTP is used as the XML-RPC transport. But digging a bit into the specification, you'll discover, that none HTTP-specific parts of the protocol are used. It's just used as a line to transport the XML data.
So you may wonder if it's possible to use XML-RPC with transport other than HTTP. In short, yes. But you may need to hack around the XML-RPC libraries (because they usually suppose you'll want to use HTTP).
PHP XML-RPC server
First, you need some class, that you want to expose with PHP XML-RPC:
class MyClass { /** * @param string $input * @return string */ public function repeat($input) { return $input; } }
Notice I've set the parameter and return type in phpdoc.
Now let's expose this class with Zend Framework XML-RPC implementation.
You need to download Zend Framework first, let's say to /path/to/zf directory.
class MyClass { /** * @param string $input * @return string */ public function repeat($input) { return $input; } } set_include_path(get_include_path() . PATH_SEPARATOR . 'zf/library'); require_once "Zend/XmlRpc/Server.php"; $server = new Zend_XmlRpc_Server(); $server->setClass('MyClass', 'myclass'); echo $server->handle();
Set_include_path line adds the /path/to/zf/library directory to PHP path, so you can import the Zend_XmlRpc_Server class (located in /path/to/zf/library/Zend/XmlRpc/Server.php file).
Then there is an instance of Zend_XmlRpc_Server created, then there is MyClass attached as the class for myclass XMLRPC namespace. This means the repeat method is to be called via the XML-RPC as myclass.repeat.
If you place the file on your server and have it under some URL, for example:
http://your-server.com/myclass.php
This URL is fully valid XML-RPC server endpoint for XML-RPC clients.
Python client
Having the XML-RPC server running we can connect to it from any XML-RPC enabled library in any programming language around.
In Python, to call the remote procedure myclass.repeat on the XML-RPC endpoint http://your-server.com/myclass.php, you would do the following:
from xmlrpclib import ServerProxy server = ServerProxy('http://your-server.com/myclass.php') print server.myclass.repeat('Hello RPC service')
Running this code:
# python xmlrpc-test.py
gives you:
Hello RPC service
Under the hood:
- Python script makes a connection to http://your-server.com/myclass.php
- your webserver runs the myclass.php script
- the $server->handle() line processes the data received
- chooses a class and a method to run (this would be MyClass and repeat)
- passes the arguments (a string 'Hello RPC service') to the method
- gets the return value
- passes it back to the client wrapped in XML-RPC protocol
- the $server->handle() line processes the data received
- your webserver runs the myclass.php script
- Python gets XML reply and converts it back to simple string ('Hello RPC service')
- and prints it on the console
Omitting the HTTP protocol
Probably you have both Python and PHP scripts to be run on the same machine, so the HTTP part is quite useless and an additional point of failure.
As I already stated, the HTTP is only a transport and you can replace it (with some cost) with some other transport.
I came into an idea to use stdout/stdin as the transport, so Python would execute a PHP script (command line interface) and pass the XML-RPC request to the script's stdin. PHP would then have to get the XML-RPC request from stdin instead of from HTTP request.
This means two modifications in server and client code.
First the server:
class MyClass { /** * @param string $input * @return string */ public function repeat($input) { return $input; } } set_include_path(get_include_path() . PATH_SEPARATOR . 'zf/library'); require_once "Zend/XmlRpc/Server.php"; require_once "Zend/XmlRpc/Request/Stdin.php"; $server = new Zend_XmlRpc_Server(); $server->setClass('MyClass', 'myclass'); echo $server->handle(new Zend_XmlRpc_Request_Stdin());
The change is passing an instance of Zend_XmlRpc_Request_Stdin to $server->handle(). This is all needed. Guys from Zend Framework already predicted such a use.
Then, the client part.
Xmlrpclib allows passing a custom transport in case you want to implement some proxies or other thing. We'll make a transport, that instead of making a HTTP connection, runs a PHP script, passes the request to its stdin and gets the response from stdout:
from xmlrpclib import Transport, Server from subprocess import Popen, PIPE class LocalFileTransport(Transport): class Connection: def setCmd(self, cmd): self.cmd = Popen(['php', cmd], stdin=PIPE, stdout=PIPE) def send(self, content): self.cmd.stdin.write(content) self.cmd.stdin.close() def getreply(self): return 200, '', [] def getfile(self): return self.cmd.stdout def make_connection(self, host): return self.Connection() def send_request(self, connection, handler, request_body): connection.setCmd(handler) def send_content(self, connection, request_body): connection.send(request_body) def send_host(self, connection, host): pass def send_user_agent(self, connection): pass server = Server('http://host.com/path/to/the/php/script/myclass.php', transport = LocalFileTransport()) print server.myclass.repeat('Hello XML-RPC with no HTTP service')
Notes:
- host.com in the URL is completely ignored, use whatever value you want
- /path/to/the/php/script/myclass.php in URL is passed as the PHP script to run
What to do next?
Having this simple skeleton, you can now extend the MyClass, actually give it more proper name first! You can also attach more classes to the XML-RPC server using different namespaces:
$server->setClass('SomeClass', 'some);
$server->setClass('MyClass', 'my');
$server->setClass('YourClass', 'your');
Only public methods are exposed to the XML-RPC clients, so you can hide some logic inside of private or protected methods and only expose what you need from given classes.
This solution is a quick way to actually use some of your well-working PHP code in your fancy-new and elegant Python application. This can help if you want to make a filesystem with Python-FUSE, but want to data be taken from PHP application.
Did it help you?
I hope this helps someone. Feel free to comment.
Comments: 1
Po GaZie 0.2
tags: dev django gaza python
17 Dec 2008 18:01
GaZa 0.2 zakończona. Spotkanie poświęcone było frameworkowi Django, który umożliwia pisanie własnych serwisów internetowych przy użyciu języka skryptowego Python i szablonów Django. Zawiera również warstwę obiektowego dostępu do bazy danych.
Spotkanie trwało półtorej godziny (16.45 do 18.15). Zdążyłem przez ten czas pokazać jak zainstalować bibilotekę Django, stworzyć pierwszy projekt i pierwszą aplikację, uruchomić deweloperski serwer WWW dołączony do Django.
Skorzystaliśmy również w prosty sposób z systemu szablonów i pokazaliśmy wyższość Django nad Ruby on Rails pokazywanym na [poprzedniej gazie.
Przyszło sporo uczestników. Np. Kamil nie robił problemów i ładnie wyglądał.
Następna Gaza prawdopodnie po świętach w poniedziałek o 16.45.
Comments: 4
Learning Django
tags: dev django python
11 Dec 2008 20:47
Today I started (again) to learn Django — framework for writing web applications in Python (rocks!).
Anyone could tell looking at my browser:
click to enlarge
Comments: 3
Hacking pympd and Last.fm
tags: dev lastfm python
26 Sep 2008 22:36
Hello again,
My girlfriend was very unhappy with me not having my played songs submitted to Last.fm social music revolution portal.
That was because used to use Music Player Daemon and its various clients. Most of the clients don't implement the AudioScrobbler protocol, but as a matter of fact this is not needed, because there can be a separate MPD client meant just to submit the info to last.fm, running in parallel to one actually playing music.
I used to use scmpc for this reason, but since I bought my new laptop and migrated to Ubuntu I quit it — because the application was not in their repo.
Today I decided to find some short python-based implementation of audioscrobbler and enrich one of MPD clients with the Last.fm integration. I found pympd really good — nice looking, having plugin architecture, clean and simple. So I decided it to be my new favorite MPD client. Then I quick-hacked some random plugin and created a new one including almost 100% source from python-scrobbler project. The plugin:
- sends "now playing" info to Last.fm on each track-change and loading of plugin
- sends "song played" info to Last.fm on track-change and plugin unload event if song was listened at least to the half of its total length and is longer than 30 seconds.
Now the Last.fm user and password are hardcoded, but I hope to create a quick-and-dirty configuration window for it.
I had some problems with time convertion. The python-scrobbler sources suggest using datetime.utcnow() method while actually using datetime.now() is giving the right results.
You can check my Last.fm records:
- July — submitted by scmpc from Gentoo — the old computer
- first records in October — submitted with MY PLUGIN
- two hours gap — before fixing UTC issue, the songs were submitted as played two hours earlier (timezone difference)
So it seems working!
It's a pretty cool Python day today.